feat(kb): admin CRUD for topics
Backs the Phase 4 topic-management table in the KnowledgeBase EspoCRM
extension ("ניהול" tab → "נושאים" panel). Four new admin endpoints:
GET /admin/kb/topics — list ALL topics including
is_active=false ones, with
source_count joined from kb_source
POST /admin/kb/topics — create. Validates slug shape
(^[a-z][a-z0-9-]{1,62}$) and
uniqueness — clean 400 on duplicate
rather than leaking UniqueViolation
PUT /admin/kb/topics/{id} — patch name, description,
system_prompt_addendum, is_active.
slug is intentionally NOT editable —
the S3 layout depends on it
(inbox/<topic_slug>/<kind>/...)
DELETE /admin/kb/topics/{id} — hard-delete; refuses with 409 if
any sources still belong to the
topic. Soft-delete (is_active=false)
is the normal "remove from dropdown"
path
Refs Task Master #15 (espocrm-extensions/KnowledgeBase)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -387,3 +387,65 @@ async def reingest_admin_source(request: Request, source_id: int):
|
||||
|
||||
asyncio.create_task(kb_admin_sources.process_reingest_job(job_id))
|
||||
return {"job_id": job_id, "status": "queued", "source_id": source_id}
|
||||
|
||||
|
||||
# ── Phase 4: topic CRUD (Task #15) ─────────────────────────────────────────
|
||||
|
||||
class TopicCreate(BaseModel):
|
||||
slug: str = Field(..., min_length=2, max_length=63)
|
||||
name: str = Field(..., min_length=1, max_length=200)
|
||||
description: str | None = Field(None, max_length=1000)
|
||||
system_prompt_addendum: str | None = Field(None, max_length=4000)
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class TopicUpdate(BaseModel):
|
||||
name: str | None = Field(None, max_length=200)
|
||||
description: str | None = Field(None, max_length=1000)
|
||||
system_prompt_addendum: str | None = Field(None, max_length=4000)
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
@router.get("/topics")
|
||||
async def list_admin_topics(request: Request):
|
||||
_verify_admin(request)
|
||||
items = await kb_topics.list_topics_admin()
|
||||
return {"items": items, "count": len(items)}
|
||||
|
||||
|
||||
@router.post("/topics")
|
||||
async def create_admin_topic(request: Request, body: TopicCreate):
|
||||
_verify_admin(request)
|
||||
try:
|
||||
return await kb_topics.create_topic(
|
||||
slug=body.slug,
|
||||
name=body.name,
|
||||
description=body.description,
|
||||
system_prompt_addendum=body.system_prompt_addendum,
|
||||
is_active=body.is_active,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/topics/{topic_id}")
|
||||
async def update_admin_topic(request: Request, topic_id: int, body: TopicUpdate):
|
||||
_verify_admin(request)
|
||||
fields = body.model_dump(exclude_unset=True)
|
||||
try:
|
||||
return await kb_topics.update_topic(topic_id, fields)
|
||||
except LookupError:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/topics/{topic_id}")
|
||||
async def delete_admin_topic(request: Request, topic_id: int):
|
||||
_verify_admin(request)
|
||||
try:
|
||||
return await kb_topics.delete_topic(topic_id)
|
||||
except LookupError:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
except kb_topics.ConflictError as e:
|
||||
raise HTTPException(status_code=409, detail=str(e))
|
||||
|
||||
Reference in New Issue
Block a user