diff --git a/agent-office/app/main.py b/agent-office/app/main.py index fbb60df..02fb4d4 100644 --- a/agent-office/app/main.py +++ b/agent-office/app/main.py @@ -12,9 +12,11 @@ from .agents import init_agents, get_agent, get_all_agent_states, AGENT_REGISTRY from .scheduler import init_scheduler from . import telegram_bot from .routers import notify as notify_router +from .routers import tarot as tarot_router app = FastAPI() app.include_router(notify_router.router) +app.include_router(tarot_router.router) _cors_origins = CORS_ALLOW_ORIGINS.split(",") app.add_middleware( diff --git a/agent-office/app/routers/tarot.py b/agent-office/app/routers/tarot.py new file mode 100644 index 0000000..277d4ec --- /dev/null +++ b/agent-office/app/routers/tarot.py @@ -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} diff --git a/agent-office/tests/test_tarot_routes.py b/agent-office/tests/test_tarot_routes.py new file mode 100644 index 0000000..3808cb6 --- /dev/null +++ b/agent-office/tests/test_tarot_routes.py @@ -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