feat: add topics CRUD endpoints
This commit is contained in:
@@ -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})
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user