From 3b665715ff332bf3403371662998de98be54a9e0 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 25 May 2026 18:45:15 +0200 Subject: [PATCH] feat: add topics CRUD endpoints --- src/backend/api/routes/topics.py | 93 ++++++++++++++++++++++++++++++++ src/backend/db/database.py | 70 ++++++++++++++++++++++++ src/backend/main.py | 2 + 3 files changed, 165 insertions(+) create mode 100644 src/backend/api/routes/topics.py diff --git a/src/backend/api/routes/topics.py b/src/backend/api/routes/topics.py new file mode 100644 index 0000000..1985e67 --- /dev/null +++ b/src/backend/api/routes/topics.py @@ -0,0 +1,93 @@ +from fastapi import APIRouter, Depends +from starlette.responses import JSONResponse + +from src.backend.api.dependencies import verify_api_key +from src.backend.db.database import ( + get_db, + create_topic, + list_topics, + get_topic, + update_topic, + delete_topic, +) +from src.backend.models.api import TopicCreate, TopicUpdate, ok, fail + +router = APIRouter( + prefix="/topics", + tags=["topics"], + dependencies=[Depends(verify_api_key)], +) + + +async def _get_db(): + db = await get_db() + try: + yield db + finally: + await db.close() + + +@router.post("", summary="Create topic") +async def create(body: TopicCreate, db=Depends(_get_db)): + try: + topic = await create_topic( + db, + name=body.name, + system_prompt=body.system_prompt, + summary=body.summary, + model=body.model, + ) + except Exception as e: + if "UNIQUE constraint failed" in str(e): + return JSONResponse( + status_code=409, + content=fail("DUPLICATE", f"Topic '{body.name}' already exists"), + ) + raise + return JSONResponse(status_code=201, content=ok(topic)) + + +@router.get("", summary="List topics") +async def list_all(db=Depends(_get_db)): + topics = await list_topics(db) + return ok(topics) + + +@router.get("/{topic_id}", summary="Get topic") +async def get_one(topic_id: str, db=Depends(_get_db)): + topic = await get_topic(db, topic_id) + if not topic: + return JSONResponse( + status_code=404, + content=fail("NOT_FOUND", "Topic not found"), + ) + return ok(topic) + + +@router.put("/{topic_id}", summary="Update topic") +async def update(topic_id: str, body: TopicUpdate, db=Depends(_get_db)): + topic = await update_topic( + db, + topic_id, + name=body.name, + system_prompt=body.system_prompt, + summary=body.summary, + model=body.model, + ) + if not topic: + return JSONResponse( + status_code=404, + content=fail("NOT_FOUND", "Topic not found"), + ) + return ok(topic) + + +@router.delete("/{topic_id}", summary="Delete topic") +async def delete(topic_id: str, db=Depends(_get_db)): + deleted = await delete_topic(db, topic_id) + if not deleted: + return JSONResponse( + status_code=404, + content=fail("NOT_FOUND", "Topic not found or has active requests"), + ) + return ok({"deleted": True}) diff --git a/src/backend/db/database.py b/src/backend/db/database.py index 173e6f3..3cecbbd 100644 --- a/src/backend/db/database.py +++ b/src/backend/db/database.py @@ -59,3 +59,73 @@ async def init_db(): await db.commit() finally: await db.close() + + +async def create_topic( + db: aiosqlite.Connection, + name: str, + system_prompt: str, + summary: str = "", + model: str | None = None, +) -> dict: + topic_id = _new_id() + now = _now() + await db.execute( + "INSERT INTO topics (id, name, system_prompt, summary, model, created_at, updated_at) " + "VALUES (?, ?, ?, ?, ?, ?, ?)", + (topic_id, name, system_prompt, summary, model, now, now), + ) + await db.commit() + return { + "id": topic_id, + "name": name, + "system_prompt": system_prompt, + "summary": summary, + "model": model, + "created_at": now, + "updated_at": now, + } + + +async def list_topics(db: aiosqlite.Connection) -> list[dict]: + cursor = await db.execute("SELECT * FROM topics ORDER BY created_at DESC") + rows = await cursor.fetchall() + return [dict(row) for row in rows] + + +async def get_topic(db: aiosqlite.Connection, topic_id: str) -> dict | None: + cursor = await db.execute("SELECT * FROM topics WHERE id = ?", (topic_id,)) + row = await cursor.fetchone() + return dict(row) if row else None + + +async def update_topic( + db: aiosqlite.Connection, topic_id: str, **fields +) -> dict | None: + existing = await get_topic(db, topic_id) + if not existing: + return None + fields = {k: v for k, v in fields.items() if v is not None} + if not fields: + return existing + fields["updated_at"] = _now() + set_clause = ", ".join(f"{k} = ?" for k in fields) + values = list(fields.values()) + [topic_id] + await db.execute( + f"UPDATE topics SET {set_clause} WHERE id = ?", values + ) + await db.commit() + return await get_topic(db, topic_id) + + +async def delete_topic(db: aiosqlite.Connection, topic_id: str) -> bool: + cursor = await db.execute( + "SELECT COUNT(*) FROM requests WHERE topic_id = ? AND status IN ('pending', 'processing')", + (topic_id,), + ) + row = await cursor.fetchone() + if row[0] > 0: + return False + cursor = await db.execute("DELETE FROM topics WHERE id = ?", (topic_id,)) + await db.commit() + return cursor.rowcount > 0 diff --git a/src/backend/main.py b/src/backend/main.py index 8fc28af..6fb728f 100644 --- a/src/backend/main.py +++ b/src/backend/main.py @@ -3,6 +3,7 @@ from contextlib import asynccontextmanager from fastapi import FastAPI from src.backend.api.routes.health import router as health_router +from src.backend.api.routes.topics import router as topics_router from src.backend.db.database import init_db @@ -15,3 +16,4 @@ async def lifespan(app: FastAPI): app = FastAPI(title="OpusAgent", version="0.1.0", lifespan=lifespan) app.include_router(health_router, prefix="/api") +app.include_router(topics_router, prefix="/api")