From 3fab343b6ee60dfa96d436df3c29ea8983ca0497 Mon Sep 17 00:00:00 2001 From: Deep Kumar Singh Kushwah Date: Thu, 11 Jun 2026 18:50:58 +0530 Subject: [PATCH] feat(gateway,admin-ui): corpus create/delete admin CRUD (Step 3.10) Corpus management in the admin console was local-only: the "New corpus" button mutated React state and never called the gateway, and the gateway had no write route (POST /v1/corpora returned 405). This wires it end to end. - CorpusStore SPI: add tenant-scoped create / delete (NoopCorpusStore + PgCorpusStore), with the same cross-tenant invisibility rule as get. - Gateway: POST /v1/corpora (201, server-minted id, tenant from ctx) and DELETE /v1/corpora/{id} (204/404). New CreateCorpusRequest wire type. - Admin UI: createCorpus / deleteCorpus client + Corpora page calls them when live, keeps local behaviour in demo mode (mirrors the Webhooks page). - Tests: gateway routes (test_corpora.py), SPI conformance, Pg integration. - Docs + regenerated dist/openapi.{json,yaml}. Verified locally (CI billing-blocked): full create -> list -> delete -> 404 lifecycle + tenant isolation + CORS preflight/POST from :3100; ruff, mypy --strict, pytest, admin-ui tsc + vitest all green. Co-Authored-By: Claude Opus 4.8 --- apps/admin-ui/src/app/corpora/page.tsx | 57 +++++-- apps/admin-ui/src/lib/api.ts | 19 +++ apps/gateway/src/rag_gateway/corpora.py | 102 ++++++++++-- apps/gateway/tests/test_corpora.py | 99 +++++++++++ dist/openapi.json | 155 ++++++++++++++++++ dist/openapi.yaml | 128 +++++++++++++++ docs/architecture/gateway.md | 2 +- docs/reference/admin-ui.md | 2 +- docs/reference/gateway.md | 39 +++++ .../rag_backends/corpus/pg_corpus_store.py | 40 ++++- packages/core/src/rag_core/gateway_types.py | 27 +++ .../core/src/rag_core/spi/corpus_store.py | 43 ++++- .../src/rag_core/spi/noop/corpus_store.py | 18 +- tests/contract/test_corpus_store.py | 27 +++ tests/integration/test_pg_corpus_store.py | 22 +++ 15 files changed, 736 insertions(+), 44 deletions(-) create mode 100644 apps/gateway/tests/test_corpora.py diff --git a/apps/admin-ui/src/app/corpora/page.tsx b/apps/admin-ui/src/app/corpora/page.tsx index 7b06f11..9616187 100644 --- a/apps/admin-ui/src/app/corpora/page.tsx +++ b/apps/admin-ui/src/app/corpora/page.tsx @@ -26,7 +26,8 @@ import { TableSkeleton, Textarea, } from "@/components/ui/primitives"; -import { fetchCorpora } from "@/lib/api"; +import { createCorpus, deleteCorpus, fetchCorpora } from "@/lib/api"; +import { GATEWAY_URL } from "@/lib/config"; import { CORPORA } from "@/lib/mock"; import { absTime, relTime } from "@/lib/time"; import type { Corpus } from "@/lib/types"; @@ -188,9 +189,35 @@ export default function CorporaPage() { {creating && ( setCreating(false)} - onCreate={(corpus) => { + onCreate={async ({ display_name, description, embedding_model, embedding_dimension }) => { + let corpus: Corpus; + if (GATEWAY_URL && source === "live") { + try { + corpus = await createCorpus(tenant.id, { + display_name, + description, + embedding_model, + embedding_dimension, + }); + } catch { + toast({ title: "Failed to create corpus", variant: "error" }); + return; + } + } else { + corpus = { + id: "corpus_" + Math.random().toString(16).slice(2, 14), + tenant_id: tenant.id, + display_name, + description, + embedding_model, + embedding_dimension, + document_count: 0, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + metadata: {}, + }; + } setRows((prev) => [corpus, ...prev]); setCreating(false); toast({ title: "Corpus created", description: corpus.display_name, variant: "success" }); @@ -214,8 +241,15 @@ export default function CorporaPage() {