feat: add topics CRUD endpoints

This commit is contained in:
2026-05-25 18:45:15 +02:00
parent 7f88ca3a36
commit 3b665715ff
3 changed files with 165 additions and 0 deletions
+93
View File
@@ -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})
+70
View File
@@ -59,3 +59,73 @@ async def init_db():
await db.commit() await db.commit()
finally: finally:
await db.close() 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
+2
View File
@@ -3,6 +3,7 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from src.backend.api.routes.health import router as health_router 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 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 = FastAPI(title="OpusAgent", version="0.1.0", lifespan=lifespan)
app.include_router(health_router, prefix="/api") app.include_router(health_router, prefix="/api")
app.include_router(topics_router, prefix="/api")