Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion .claude/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,18 @@
"-c",
"cd apps/admin-ui && exec node node_modules/next/dist/bin/next dev -p 3100"
],
"port": 3100
"port": 3100,
"autoPort": false
},
{
"name": "admin-ui-preview",
"runtimeExecutable": "sh",
"runtimeArgs": [
"-c",
"cd apps/admin-ui && export NEXT_PUBLIC_GATEWAY_URL='' && exec node node_modules/next/dist/bin/next dev -p 3101"
],
"port": 3101,
"autoPort": false
},
{
"name": "docs-site",
Expand All @@ -31,6 +42,16 @@
"cd website && exec npm run serve -- --port 3200 --no-open"
],
"port": 3200
},
{
"name": "rube-local",
"runtimeExecutable": "sh",
"runtimeArgs": [
"-c",
"cd /Users/deepkumarsinghkushwah/Code/GitHub/rube && exec npm run preview:local"
],
"port": 7654,
"autoPort": false
}
]
}
22 changes: 22 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Both images (Dockerfile, apps/admin-ui/Dockerfile) build from the repo root,
# so host-only / platform-specific dirs must not leak into the build context —
# most importantly node_modules, which would clobber the Linux deps the
# admin-ui builder installs (macOS-native binaries break `next build`).
.git
**/.DS_Store

# Node — the admin-ui builder installs fresh Linux deps; never copy host ones.
**/node_modules
**/.next

# Python — venvs + caches are rebuilt inside the image.
.venv
**/.venv
**/__pycache__
**/.pytest_cache
**/.mypy_cache
**/.ruff_cache
**/*.pyc

# Local-only config must not bake into images (configure via build args / mounts).
**/.env*.local
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ __marimo__/
# Streamlit
.streamlit/secrets.toml

# macOS
.DS_Store
# macOS duplicate files (e.g. "foo 2.py")
* 2.*

Expand Down
284 changes: 284 additions & 0 deletions BUGS.md

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
WORKDIR /app

# Optional uv extras baked into the gateway venv. Empty by default keeps the
# production image lean (noop wiring serves every route with zero infra). Set
# GATEWAY_EXTRAS=backends to pull in rag-backends (asyncpg, etc.) so a
# config-driven serve can use the Postgres corpus store / real backends.
ARG GATEWAY_EXTRAS=""

# Copy workspace manifests first so this layer is cached unless deps change.
COPY pyproject.toml uv.lock ./
COPY packages/core/pyproject.toml packages/core/
Expand All @@ -20,7 +26,7 @@ COPY packages/ packages/
COPY apps/gateway/ apps/gateway/
COPY sdks/python/ sdks/python/
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
uv sync --frozen --no-dev ${GATEWAY_EXTRAS:+--extra ${GATEWAY_EXTRAS}}

# ── Runtime stage ─────────────────────────────────────────────────────────────
FROM python:3.12-slim-bookworm AS runtime
Expand Down
9 changes: 9 additions & 0 deletions apps/admin-ui/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,22 @@ FROM node:20-bookworm-slim AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY apps/admin-ui/ ./
# Next.js inlines NEXT_PUBLIC_* into the client bundle at build time, so the
# gateway URL must be present during `next build` (a runtime -e is too late).
# Pass --build-arg NEXT_PUBLIC_GATEWAY_URL=http://localhost:8000 to wire the
# console to a live gateway; left empty, the UI falls back to demo/seed data.
ARG NEXT_PUBLIC_GATEWAY_URL=""
ENV NEXT_PUBLIC_GATEWAY_URL=$NEXT_PUBLIC_GATEWAY_URL
RUN npm run build

# Minimal runtime: just the standalone server + static assets, non-root.
FROM node:20-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3100
# Bind the standalone server to all interfaces so the published port is
# reachable from outside the container (default would be localhost only).
ENV HOSTNAME=0.0.0.0
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
Expand Down
3 changes: 3 additions & 0 deletions apps/admin-ui/public/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Keeps apps/admin-ui/public/ present so the Docker build's
# `COPY --from=builder /app/public ./public` step succeeds even when the
# console ships no static assets. Drop static files (favicon, images) here.
2 changes: 1 addition & 1 deletion apps/admin-ui/src/app/api-keys/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function ApiKeysPage() {
/>
<div className="mb-4">
<PreviewBanner>
API-key management is a preview — the backend lands with Phase 6 governance. Data shown here is local.
API-key management is a preview — the data shown here is local and not yet backed by the gateway.
</PreviewBanner>
</div>

Expand Down
10 changes: 9 additions & 1 deletion apps/admin-ui/src/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function RagSummary({ msg }: { msg: ThreadMessage }) {

function ChatTab({ models }: { models: ModelInfo[] }) {
const { tenant } = useApp();
const { rows: corpora } = useLive<Corpus>(
const { rows: corpora, source: corporaSource } = useLive<Corpus>(
(tid) => CORPORA.filter((c) => c.tenant_id === tid),
fetchCorpora,
);
Expand All @@ -157,6 +157,14 @@ function ChatTab({ models }: { models: ModelInfo[] }) {
setMessages([]);
setRagCorpusIds([]);
}, [tenant.id]);
// Default RAG scope to all of the tenant's live corpora once loaded, so chat
// grounds on your data with an explicit corpus_ids (empty would search all
// anyway, but the explicit payload is clearer). Live data only.
useEffect(() => {
if (corporaSource === "live" && corpora.length > 0) {
setRagCorpusIds(corpora.map((c) => c.id));
}
}, [corporaSource, corpora]);
useEffect(
() => () => {
if (streamTimer.current) clearInterval(streamTimer.current);
Expand Down
95 changes: 93 additions & 2 deletions apps/admin-ui/src/app/corpora/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
TableSkeleton,
Textarea,
} from "@/components/ui/primitives";
import { createCorpus, deleteCorpus, fetchCorpora } from "@/lib/api";
import { createCorpus, deleteCorpus, fetchCorpora, updateCorpus } from "@/lib/api";
import { GATEWAY_URL } from "@/lib/config";
import { CORPORA } from "@/lib/mock";
import { absTime, relTime } from "@/lib/time";
Expand All @@ -50,6 +50,7 @@ export default function CorporaPage() {

const [selected, setSelected] = useState<Corpus | null>(null);
const [creating, setCreating] = useState(false);
const [editing, setEditing] = useState<Corpus | null>(null);
const [deleting, setDeleting] = useState<Corpus | null>(null);

return (
Expand Down Expand Up @@ -127,7 +128,9 @@ export default function CorporaPage() {
<DropdownItem icon={<Database className="h-4 w-4" />} onClick={() => setSelected(c)}>
View details
</DropdownItem>
<DropdownItem icon={<Pencil className="h-4 w-4" />}>Edit</DropdownItem>
<DropdownItem icon={<Pencil className="h-4 w-4" />} onClick={() => setEditing(c)}>
Edit
</DropdownItem>
<DropdownSeparator />
<DropdownItem danger icon={<Trash className="h-4 w-4" />} onClick={() => setDeleting(c)}>
Delete
Expand Down Expand Up @@ -225,6 +228,34 @@ export default function CorporaPage() {
/>
)}

{editing && (
<EditCorpusDialog
corpus={editing}
onClose={() => setEditing(null)}
onSave={async ({ display_name, description }) => {
let updated: Corpus;
if (GATEWAY_URL && source === "live") {
try {
updated = await updateCorpus(tenant.id, editing.id, { display_name, description });
} catch {
toast({ title: "Failed to update corpus", variant: "error" });
return;
}
} else {
updated = {
...editing,
display_name,
description,
updated_at: new Date().toISOString(),
};
}
setRows((prev) => prev.map((c) => (c.id === updated.id ? updated : c)));
setEditing(null);
toast({ title: "Corpus updated", description: updated.display_name, variant: "success" });
}}
/>
)}

<Dialog
open={!!deleting}
onClose={() => setDeleting(null)}
Expand Down Expand Up @@ -326,3 +357,63 @@ function NewCorpusDialog({
</Dialog>
);
}

function EditCorpusDialog({
corpus,
onClose,
onSave,
}: {
corpus: Corpus;
onClose: () => void;
onSave: (v: { display_name: string; description: string }) => void;
}) {
const [name, setName] = useState(corpus.display_name);
const [description, setDescription] = useState(corpus.description ?? "");
const dirty =
name.trim() !== corpus.display_name || description.trim() !== (corpus.description ?? "");

return (
<Dialog
open
onClose={onClose}
title="Edit corpus"
description="Update this corpus's display name and description."
size="md"
>
<div className="space-y-4">
<Field label="Display name">
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Support Knowledge Base"
/>
</Field>
<Field label="Description">
<Textarea value={description} onChange={(e) => setDescription(e.target.value)} />
</Field>
<div className="grid grid-cols-2 gap-4">
<Field label="Embedding model">
<Input value={corpus.embedding_model ?? ""} readOnly disabled className="bg-muted/40 font-mono" />
</Field>
<Field label="Dimension">
<Input value={corpus.embedding_dimension ?? ""} readOnly disabled className="bg-muted/40 font-mono" />
</Field>
</div>
<p className="text-xs text-muted-foreground">
Embedding model and dimension are fixed at creation — they define the vector space.
</p>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button
disabled={!name.trim() || !dirty}
onClick={() => onSave({ display_name: name.trim(), description: description.trim() })}
>
Save changes
</Button>
</DialogFooter>
</Dialog>
);
}
13 changes: 13 additions & 0 deletions apps/admin-ui/src/app/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion apps/admin-ui/src/app/metrics/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ function Proc({ label, value }: { label: string; value: string }) {
return (
<div>
<div className="text-xs uppercase tracking-wide text-muted-foreground">{label}</div>
<div className="mt-1 truncate font-mono text-sm" title={value}>
<div className="mt-1 truncate font-mono text-sm" title={value} suppressHydrationWarning>
{value}
</div>
</div>
Expand Down
14 changes: 12 additions & 2 deletions apps/admin-ui/src/app/playground/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export default function PlaygroundPage() {
const { tenant } = useApp();
const { toast } = useToast();
const router = useRouter();
const { rows: corpora } = useLive<Corpus>(
const { rows: corpora, source: corporaSource } = useLive<Corpus>(
(tid) => CORPORA.filter((c) => c.tenant_id === tid),
fetchCorpora,
);
Expand Down Expand Up @@ -212,6 +212,16 @@ export default function PlaygroundPage() {
lastRunRef.current = null;
}, [tenant.id, stopStream]);

// Default the corpus scope to ALL of the tenant's live corpora once they load,
// so a query goes out with an explicit corpus_ids instead of an empty list.
// (The gateway treats empty as "all" anyway, but the empty payload is
// confusing.) Live data only — never the mock fallback's synthetic ids.
useEffect(() => {
if (corporaSource === "live" && corpora.length > 0) {
setCorpusIds(corpora.map((c) => c.id));
}
}, [corporaSource, corpora]);

function streamAnswer(text: string) {
if (prefersReducedMotion()) {
setStreamText(text);
Expand Down Expand Up @@ -352,7 +362,7 @@ export default function PlaygroundPage() {
<PageHeader
title="Query Playground"
description="Run a query through the full pipeline — understand → route → retrieve → rerank → pack → generate — and inspect every stage."
badge={source === "mock" ? <DemoDataBadge /> : undefined}
badge={phase !== "idle" && source === "mock" ? <DemoDataBadge /> : undefined}
/>

<div className="grid items-start gap-6 xl:grid-cols-[340px_minmax(0,1fr)]">
Expand Down
Loading
Loading