feat(agent-office): service_proxy pipeline_retry/list_failed_pipelines (+ music-lab status=failed 필터)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 00:33:28 +09:00
parent ef1a7a92fd
commit d048251a97
4 changed files with 53 additions and 1 deletions

View File

@@ -352,6 +352,25 @@ async def list_active_pipelines() -> list[dict]:
return resp.json().get("pipelines", []) return resp.json().get("pipelines", [])
async def list_failed_pipelines() -> list[dict]:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(f"{MUSIC_LAB_URL}/api/music/pipeline?status=failed")
resp.raise_for_status()
data = resp.json()
return data if isinstance(data, list) else data.get("items", data.get("pipelines", []))
async def pipeline_retry(pid: int) -> dict:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.post(f"{MUSIC_LAB_URL}/api/music/pipeline/{pid}/retry")
out = {"status_code": resp.status_code}
try:
out.update(resp.json())
except Exception:
pass
return out
async def get_pipeline(pid: int) -> dict: async def get_pipeline(pid: int) -> dict:
async with httpx.AsyncClient(timeout=15) as client: async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(f"{MUSIC_LAB_URL}/api/music/pipeline/{pid}") resp = await client.get(f"{MUSIC_LAB_URL}/api/music/pipeline/{pid}")

View File

@@ -1135,6 +1135,21 @@ def list_pipelines(active_only: bool = False) -> List[Dict[str, Any]]:
return [_parse_pipeline_row(r) for r in rows] return [_parse_pipeline_row(r) for r in rows]
def list_pipelines_by_state(state: str) -> List[Dict[str, Any]]:
"""특정 state의 파이프라인만 조회 (예: 'failed')."""
sql = """
SELECT vp.*, ml.title AS track_title, cj.title AS compile_title
FROM video_pipelines vp
LEFT JOIN music_library ml ON ml.id = vp.track_id
LEFT JOIN compile_jobs cj ON cj.id = vp.compile_job_id
WHERE vp.state = ?
ORDER BY vp.created_at DESC
"""
with _conn() as conn:
rows = conn.execute(sql, (state,)).fetchall()
return [_parse_pipeline_row(r) for r in rows]
def increment_feedback_count(pid: int, step: str) -> int: def increment_feedback_count(pid: int, step: str) -> int:
"""원자적으로 feedback_count_per_step.<step>를 +1 한 뒤 새 값을 반환. """원자적으로 feedback_count_per_step.<step>를 +1 한 뒤 새 값을 반환.

View File

@@ -1030,7 +1030,12 @@ def create_pipeline(req: PipelineCreate):
@app.get("/api/music/pipeline") @app.get("/api/music/pipeline")
def list_pipelines_endpoint(status: str = "all"): def list_pipelines_endpoint(status: str = "all"):
pipelines = _db_module.list_pipelines(active_only=(status == "active")) if status == "active":
pipelines = _db_module.list_pipelines(active_only=True)
elif status == "failed":
pipelines = _db_module.list_pipelines_by_state("failed")
else:
pipelines = _db_module.list_pipelines(active_only=False)
return {"pipelines": pipelines} return {"pipelines": pipelines}

View File

@@ -52,6 +52,19 @@ def test_list_pipelines_active_filter(client):
assert all(p["state"] != "published" for p in r.json()["pipelines"]) assert all(p["state"] != "published" for p in r.json()["pipelines"])
def test_list_pipelines_failed_filter(client):
"""status=failed 필터는 state='failed' 파이프라인만 반환한다."""
# failed 파이프라인 생성
pid_f = client.post("/api/music/pipeline", json={"track_id": 1}).json()["id"]
db.update_pipeline_state(pid_f, "failed", failed_reason="cover: oops")
r = client.get("/api/music/pipeline?status=failed")
assert r.status_code == 200
pipelines = r.json()["pipelines"]
assert len(pipelines) == 1
assert pipelines[0]["state"] == "failed"
assert pipelines[0]["id"] == pid_f
def test_feedback_reject_records_feedback_and_increments_count(client): def test_feedback_reject_records_feedback_and_increments_count(client):
pid = client.post("/api/music/pipeline", json={"track_id": 1}).json()["id"] pid = client.post("/api/music/pipeline", json={"track_id": 1}).json()["id"]
db.update_pipeline_state(pid, "cover_pending") db.update_pipeline_state(pid, "cover_pending")