feat(agent-office): /activity 통합 피드에 필터 추가 (agent_id/type/status/days)
오버사이트 UI용. get_activity_feed가 브랜치별 WHERE로 필터, total도 동일 반영. status는 task 전용(주면 log 제외). 값은 ? 바인딩, type은 브랜치 선택만이라 injection 안전. 신규 5 테스트. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -360,6 +360,7 @@ AI 에이전트 가상 오피스 — 기존 서비스 API를 프록시로 호출
|
|||||||
| POST | `/api/agent-office/telegram/webhook` | 텔레그램 Webhook (realestate_bookmark_* 콜백 포함) |
|
| POST | `/api/agent-office/telegram/webhook` | 텔레그램 Webhook (realestate_bookmark_* 콜백 포함) |
|
||||||
| POST | `/api/agent-office/realestate/notify` | realestate-lab 전용 push 수신 → 텔레그램 |
|
| POST | `/api/agent-office/realestate/notify` | realestate-lab 전용 push 수신 → 텔레그램 |
|
||||||
| GET | `/api/agent-office/states` | 전체 에이전트 상태 |
|
| GET | `/api/agent-office/states` | 전체 에이전트 상태 |
|
||||||
|
| GET | `/api/agent-office/activity` | 전 에이전트 통합 활동 피드 (tasks+logs UNION). 필터 `agent_id`/`type`(task\|log)/`status`/`days` + `limit`/`offset` |
|
||||||
| GET | `/api/agent-office/conversation/stats` | 텔레그램 대화 토큰·캐시 통계 (`days`) |
|
| GET | `/api/agent-office/conversation/stats` | 텔레그램 대화 토큰·캐시 통계 (`days`) |
|
||||||
| POST/GET | `/api/agent-office/youtube/research` (+ `/status`) | YouTube 트렌드 수집 트리거/상태 |
|
| POST/GET | `/api/agent-office/youtube/research` (+ `/status`) | YouTube 트렌드 수집 트리거/상태 |
|
||||||
| GET | `/api/agent-office/lotto/signals`, `/lotto/baselines` | 로또 시그널 이력·baseline |
|
| GET | `/api/agent-office/lotto/signals`, `/lotto/baselines` | 로또 시그널 이력·baseline |
|
||||||
|
|||||||
@@ -534,33 +534,58 @@ def get_conversation_stats(days: int = 7) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_activity_feed(limit: int = 50, offset: int = 0) -> dict:
|
def get_activity_feed(limit: int = 50, offset: int = 0, agent_id: str = None,
|
||||||
with _conn() as conn:
|
type: str = None, status: str = None, days: int = None) -> dict:
|
||||||
total_row = conn.execute("""
|
# 브랜치별 WHERE (값은 ? 바인딩, type은 브랜치 선택용). status는 task 전용 → 주면 log 제외.
|
||||||
SELECT (SELECT COUNT(*) FROM agent_tasks) + (SELECT COUNT(*) FROM agent_logs) AS total
|
task_where, task_params = [], []
|
||||||
""").fetchone()
|
log_where, log_params = [], []
|
||||||
total = total_row["total"] if total_row else 0
|
if agent_id:
|
||||||
|
task_where.append("agent_id=?"); task_params.append(agent_id)
|
||||||
|
log_where.append("agent_id=?"); log_params.append(agent_id)
|
||||||
|
if status:
|
||||||
|
task_where.append("status=?"); task_params.append(status)
|
||||||
|
if days and days > 0:
|
||||||
|
task_where.append("created_at >= datetime('now', ?)"); task_params.append(f"-{int(days)} days")
|
||||||
|
log_where.append("created_at >= datetime('now', ?)"); log_params.append(f"-{int(days)} days")
|
||||||
|
include_tasks = type in (None, "task")
|
||||||
|
include_logs = type in (None, "log") and not status
|
||||||
|
|
||||||
rows = conn.execute("""
|
task_clause = (" WHERE " + " AND ".join(task_where)) if task_where else ""
|
||||||
|
log_clause = (" WHERE " + " AND ".join(log_where)) if log_where else ""
|
||||||
|
|
||||||
|
branches, branch_params = [], []
|
||||||
|
if include_tasks:
|
||||||
|
branches.append(f"""
|
||||||
SELECT 'task' AS type, agent_id, id AS task_id, task_type,
|
SELECT 'task' AS type, agent_id, id AS task_id, task_type,
|
||||||
status, NULL AS level,
|
status, NULL AS level,
|
||||||
COALESCE(
|
COALESCE(json_extract(result_data, '$.summary'), task_type) AS message,
|
||||||
json_extract(result_data, '$.summary'),
|
created_at, completed_at, result_data
|
||||||
task_type
|
FROM agent_tasks{task_clause}""")
|
||||||
) AS message,
|
branch_params += task_params
|
||||||
created_at, completed_at,
|
if include_logs:
|
||||||
result_data
|
branches.append(f"""
|
||||||
FROM agent_tasks
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'log' AS type, agent_id, task_id, NULL AS task_type,
|
SELECT 'log' AS type, agent_id, task_id, NULL AS task_type,
|
||||||
NULL AS status, level,
|
NULL AS status, level, message,
|
||||||
message,
|
created_at, NULL AS completed_at, NULL AS result_data
|
||||||
created_at, NULL AS completed_at,
|
FROM agent_logs{log_clause}""")
|
||||||
NULL AS result_data
|
branch_params += log_params
|
||||||
FROM agent_logs
|
|
||||||
ORDER BY created_at DESC
|
if not branches:
|
||||||
LIMIT ? OFFSET ?
|
return {"items": [], "total": 0}
|
||||||
""", (limit, offset)).fetchall()
|
|
||||||
|
union_sql = " UNION ALL ".join(branches) + " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
||||||
|
|
||||||
|
with _conn() as conn:
|
||||||
|
total = 0
|
||||||
|
if include_tasks:
|
||||||
|
total += conn.execute(
|
||||||
|
f"SELECT COUNT(*) AS c FROM agent_tasks{task_clause}", task_params
|
||||||
|
).fetchone()["c"]
|
||||||
|
if include_logs:
|
||||||
|
total += conn.execute(
|
||||||
|
f"SELECT COUNT(*) AS c FROM agent_logs{log_clause}", log_params
|
||||||
|
).fetchone()["c"]
|
||||||
|
rows = conn.execute(union_sql, branch_params + [limit, offset]).fetchall()
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
|
|||||||
@@ -198,8 +198,9 @@ def conversation_stats(days: int = 7):
|
|||||||
return get_conversation_stats(days)
|
return get_conversation_stats(days)
|
||||||
|
|
||||||
@app.get("/api/agent-office/activity")
|
@app.get("/api/agent-office/activity")
|
||||||
def activity_feed(limit: int = 50, offset: int = 0):
|
def activity_feed(limit: int = 50, offset: int = 0, agent_id: str | None = None,
|
||||||
return get_activity_feed(limit, offset)
|
type: str | None = None, status: str | None = None, days: int | None = None):
|
||||||
|
return get_activity_feed(limit, offset, agent_id=agent_id, type=type, status=status, days=days)
|
||||||
|
|
||||||
|
|
||||||
# --- Realestate Agent Push Endpoint ---
|
# --- Realestate Agent Push Endpoint ---
|
||||||
|
|||||||
76
agent-office/tests/test_activity_feed_filters.py
Normal file
76
agent-office/tests/test_activity_feed_filters.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# agent-office/tests/test_activity_feed_filters.py
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import gc
|
||||||
|
|
||||||
|
_fd, _TMP = tempfile.mkstemp(suffix=".db")
|
||||||
|
os.close(_fd)
|
||||||
|
os.unlink(_TMP)
|
||||||
|
os.environ["AGENT_OFFICE_DB_PATH"] = _TMP
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from app import db
|
||||||
|
|
||||||
|
db.DB_PATH = _TMP
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def fresh_db():
|
||||||
|
db.DB_PATH = _TMP
|
||||||
|
gc.collect()
|
||||||
|
if os.path.exists(_TMP):
|
||||||
|
os.remove(_TMP)
|
||||||
|
db.init_db()
|
||||||
|
yield
|
||||||
|
gc.collect()
|
||||||
|
if os.path.exists(_TMP):
|
||||||
|
try:
|
||||||
|
os.remove(_TMP)
|
||||||
|
except PermissionError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_by_agent_id():
|
||||||
|
db.create_task("lotto", "curate", {})
|
||||||
|
db.create_task("stock", "brief", {})
|
||||||
|
db.add_log("stock", "stock 로그")
|
||||||
|
feed = db.get_activity_feed(limit=50, offset=0, agent_id="lotto")
|
||||||
|
assert feed["total"] == 1
|
||||||
|
assert all(i["agent_id"] == "lotto" for i in feed["items"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_type_task_excludes_logs():
|
||||||
|
db.create_task("lotto", "curate", {})
|
||||||
|
db.add_log("lotto", "로그 한 줄")
|
||||||
|
feed = db.get_activity_feed(limit=50, offset=0, type="task")
|
||||||
|
assert feed["total"] == 1
|
||||||
|
assert all(i["type"] == "task" for i in feed["items"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_type_log_excludes_tasks():
|
||||||
|
db.create_task("lotto", "curate", {})
|
||||||
|
db.add_log("lotto", "로그 한 줄")
|
||||||
|
feed = db.get_activity_feed(limit=50, offset=0, type="log")
|
||||||
|
assert feed["total"] == 1
|
||||||
|
assert all(i["type"] == "log" for i in feed["items"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_status_tasks_only():
|
||||||
|
t1 = db.create_task("lotto", "curate", {})
|
||||||
|
t2 = db.create_task("lotto", "curate", {})
|
||||||
|
db.update_task_status(t1, "succeeded", {})
|
||||||
|
db.update_task_status(t2, "failed", {})
|
||||||
|
db.add_log("lotto", "로그 한 줄") # status 필터 시 log는 제외돼야 함
|
||||||
|
feed = db.get_activity_feed(limit=50, offset=0, status="succeeded")
|
||||||
|
assert feed["total"] == 1
|
||||||
|
assert all(i["type"] == "task" and i["status"] == "succeeded" for i in feed["items"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_filters_returns_all():
|
||||||
|
db.create_task("lotto", "curate", {})
|
||||||
|
db.add_log("stock", "로그")
|
||||||
|
feed = db.get_activity_feed(limit=50, offset=0)
|
||||||
|
assert feed["total"] == 2
|
||||||
Reference in New Issue
Block a user