feat(agent-office): /api/agent-office/tarot 5 endpoint (T6)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,9 +12,11 @@ from .agents import init_agents, get_agent, get_all_agent_states, AGENT_REGISTRY
|
|||||||
from .scheduler import init_scheduler
|
from .scheduler import init_scheduler
|
||||||
from . import telegram_bot
|
from . import telegram_bot
|
||||||
from .routers import notify as notify_router
|
from .routers import notify as notify_router
|
||||||
|
from .routers import tarot as tarot_router
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.include_router(notify_router.router)
|
app.include_router(notify_router.router)
|
||||||
|
app.include_router(tarot_router.router)
|
||||||
|
|
||||||
_cors_origins = CORS_ALLOW_ORIGINS.split(",")
|
_cors_origins = CORS_ALLOW_ORIGINS.split(",")
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
|
|||||||
70
agent-office/app/routers/tarot.py
Normal file
70
agent-office/app/routers/tarot.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
"""Tarot Lab 엔드포인트 — interpret + readings CRUD."""
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from ..models import (
|
||||||
|
TarotInterpretRequest,
|
||||||
|
TarotInterpretResponse,
|
||||||
|
TarotSaveRequest,
|
||||||
|
TarotPatchRequest,
|
||||||
|
)
|
||||||
|
from ..tarot import pipeline
|
||||||
|
from .. import db as db_module
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/agent-office/tarot")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/interpret", response_model=TarotInterpretResponse)
|
||||||
|
async def interpret_endpoint(req: TarotInterpretRequest):
|
||||||
|
try:
|
||||||
|
result = await pipeline.interpret(req)
|
||||||
|
except pipeline.TarotError as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/readings")
|
||||||
|
async def save_reading(req: TarotSaveRequest):
|
||||||
|
rid = db_module.save_tarot_reading(req.model_dump())
|
||||||
|
row = db_module.get_tarot_reading(rid)
|
||||||
|
return {"id": rid, "created_at": row["created_at"]}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/readings")
|
||||||
|
async def list_readings(
|
||||||
|
page: int = 1,
|
||||||
|
size: int = 20,
|
||||||
|
favorite: bool | None = None,
|
||||||
|
spread_type: str | None = None,
|
||||||
|
category: str | None = None,
|
||||||
|
):
|
||||||
|
return db_module.list_tarot_readings(
|
||||||
|
page=page, size=size,
|
||||||
|
favorite=favorite, spread_type=spread_type, category=category,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/readings/{reading_id}")
|
||||||
|
async def get_reading(reading_id: int):
|
||||||
|
row = db_module.get_tarot_reading(reading_id)
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="reading not found")
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/readings/{reading_id}")
|
||||||
|
async def patch_reading(reading_id: int, req: TarotPatchRequest):
|
||||||
|
row = db_module.get_tarot_reading(reading_id)
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="reading not found")
|
||||||
|
db_module.update_tarot_reading(reading_id, **req.model_dump(exclude_none=True))
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/readings/{reading_id}")
|
||||||
|
async def delete_reading(reading_id: int):
|
||||||
|
row = db_module.get_tarot_reading(reading_id)
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="reading not found")
|
||||||
|
db_module.delete_tarot_reading(reading_id)
|
||||||
|
return {"ok": True}
|
||||||
86
agent-office/tests/test_tarot_routes.py
Normal file
86
agent-office/tests/test_tarot_routes.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app import db as db_module
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def fresh_db(monkeypatch, tmp_path):
|
||||||
|
db_file = tmp_path / "test_routes.db"
|
||||||
|
monkeypatch.setattr(db_module, "DB_PATH", str(db_file))
|
||||||
|
db_module.init_db()
|
||||||
|
from app.main import app
|
||||||
|
yield app
|
||||||
|
|
||||||
|
|
||||||
|
def test_interpret_calls_pipeline(monkeypatch, fresh_db):
|
||||||
|
async def fake_interpret(req):
|
||||||
|
return {
|
||||||
|
"interpretation_json": {"summary": "S", "cards": [], "interactions": [], "advice": "A", "warning": None, "confidence": "high"},
|
||||||
|
"model": "claude-sonnet-4-6", "tokens_in": 100, "tokens_out": 200,
|
||||||
|
"cost_usd": 0.005, "latency_ms": 1234, "reroll_count": 0,
|
||||||
|
}
|
||||||
|
from app.tarot import pipeline
|
||||||
|
monkeypatch.setattr(pipeline, "interpret", fake_interpret)
|
||||||
|
client = TestClient(fresh_db)
|
||||||
|
r = client.post("/api/agent-office/tarot/interpret", json={
|
||||||
|
"spread_type": "one_card",
|
||||||
|
"category": "일반",
|
||||||
|
"question": "Q",
|
||||||
|
"cards": [{"position": "오늘", "card_id": "the-fool", "reversed": False}],
|
||||||
|
"cards_reference": "REF",
|
||||||
|
"context_meta": {},
|
||||||
|
})
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert r.json()["interpretation_json"]["confidence"] == "high"
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_and_list(fresh_db):
|
||||||
|
client = TestClient(fresh_db)
|
||||||
|
save = client.post("/api/agent-office/tarot/readings", json={
|
||||||
|
"spread_type": "three_card", "category": "연애", "question": "Q",
|
||||||
|
"cards": [{"position": "과거", "card_id": "the-fool", "reversed": False}],
|
||||||
|
"interpretation_json": {"summary": "S", "cards": [], "interactions": [], "advice": "A", "warning": None, "confidence": "medium"},
|
||||||
|
"model": "claude-sonnet-4-6", "tokens_in": 1, "tokens_out": 2, "cost_usd": 0.01,
|
||||||
|
"confidence": "medium",
|
||||||
|
})
|
||||||
|
assert save.status_code == 200, save.text
|
||||||
|
rid = save.json()["id"]
|
||||||
|
lst = client.get("/api/agent-office/tarot/readings?page=1&size=10")
|
||||||
|
assert lst.json()["total"] == 1
|
||||||
|
assert lst.json()["items"][0]["id"] == rid
|
||||||
|
|
||||||
|
|
||||||
|
def test_patch_favorite(fresh_db):
|
||||||
|
client = TestClient(fresh_db)
|
||||||
|
save = client.post("/api/agent-office/tarot/readings", json={
|
||||||
|
"spread_type": "one_card", "cards": [],
|
||||||
|
"interpretation_json": {"summary": "S", "cards": [], "interactions": [], "advice": "A", "warning": None, "confidence": "low"},
|
||||||
|
"model": "x", "tokens_in": 0, "tokens_out": 0, "cost_usd": 0.0, "confidence": "low",
|
||||||
|
})
|
||||||
|
rid = save.json()["id"]
|
||||||
|
p = client.patch(f"/api/agent-office/tarot/readings/{rid}", json={"favorite": True})
|
||||||
|
assert p.status_code == 200
|
||||||
|
g = client.get(f"/api/agent-office/tarot/readings/{rid}")
|
||||||
|
assert g.json()["favorite"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete(fresh_db):
|
||||||
|
client = TestClient(fresh_db)
|
||||||
|
save = client.post("/api/agent-office/tarot/readings", json={
|
||||||
|
"spread_type": "one_card", "cards": [],
|
||||||
|
"interpretation_json": {"summary": "S", "cards": [], "interactions": [], "advice": "A", "warning": None, "confidence": "low"},
|
||||||
|
"model": "x", "tokens_in": 0, "tokens_out": 0, "cost_usd": 0.0, "confidence": "low",
|
||||||
|
})
|
||||||
|
rid = save.json()["id"]
|
||||||
|
d = client.delete(f"/api/agent-office/tarot/readings/{rid}")
|
||||||
|
assert d.status_code == 200
|
||||||
|
g = client.get(f"/api/agent-office/tarot/readings/{rid}")
|
||||||
|
assert g.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_missing_reading_404(fresh_db):
|
||||||
|
client = TestClient(fresh_db)
|
||||||
|
r = client.get("/api/agent-office/tarot/readings/99999")
|
||||||
|
assert r.status_code == 404
|
||||||
Reference in New Issue
Block a user