diff --git a/TRACKER.md b/TRACKER.md index d15c09d..e96115f 100644 --- a/TRACKER.md +++ b/TRACKER.md @@ -13,13 +13,14 @@ | | | |---|---| -| **Last updated** | 2026-06-10 | +| **Last updated** | 2026-06-11 | | **Current phase** | Phase 7 — Pilot, Harden, GA (**10 / 10 steps ✅**) — **all phases complete** | | **Overall** | **84 / 84 steps ✅ — v1.0.0 GA** | | **Next action** | **🎉 GA reached.** Engineering scope (Phases 0–7) complete; `v1.0.0` cut. Remaining items are external/process (external pentest, SOC 2 audit, registry first-publish, signed referenceable customers, on-call staffing + status page, launch assets) — tracked in the [GA readiness checklist](docs/ga/ga-readiness-checklist.md). Post-GA: cut the **v1.0.1** patch tag (serve-path fix below; note: GitHub Actions runs are currently blocked by an account **billing/spending-limit** failure — every `docker.yml` run since GA failed at job start, so the GHCR image was never actually published), then the V1.1 backlog. | **Recently shipped** +- **7.9b** ✅ Admin-ui **Operate pages** — the console can now *drive* the request path, not just observe it (closes the three High-priority gaps from the [v2 API-coverage analysis](apps/admin-ui/design/claude-design-prompt-v2-api-coverage.md); built to the [Operate design brief](apps/admin-ui/design/claude-design-prompt-operate.md), 1:1 on the real wire types in `rag_core.gateway_types` / `rag_core.openai_types`). New **Operate** nav group + three pages: **Query Playground** (`/playground` — full `QueryRequest` control rail: corpus multiselect / `top_k` / rerank·pack·generate switches / advanced `pack_budget`·`generate_max_tokens`·`generate_temperature`·`filters`; **Run query** `POST /v1/query` vs **Retrieve only** `POST /v1/retrieve`; results render the decision strip (shape · bm25-fallback · corpus strategy · packed tokens · experiment tag), the answer with clickable `[n]` citation chips + guard verdict, citations, the ranked-chunks table (trust badges, detail sheet), a stacked per-stage timings bar with skipped-stage markers, and a **feedback bar** (👍/👎/1–5★/copy/regenerate/PII-redacted comment → `POST /v1/feedback`, exact signal enum); **View full trace** deep-links to `/trace?rid=…` — the seeded run reuses `DEMO_REQUEST_ID` so the demo resolves end-to-end); **Ingest** (`/ingest`, Knowledge — drag-and-drop multipart `POST /v1/ingest/document`, staged queue with progress → `IngestResult` chunk/embedding badges or error+Retry, the *parse → chunk → enrich → PII-redact → embed → index* explainer); **Chat** (`/chat` — OpenAI-compatible: streaming SSE thread with the non-standard `rag` annotations surfaced per reply (shape/chunks/citations/guard/finish_reason/usage; RAG **on by default**), an **Embeddings** tab (one input per line, float/base64, dim + L2 norm + sparkline), and a **Models** tab (`GET /v1/models`)). All follow the live→seed hybrid: action surfaces *simulate* without a gateway (deterministic worked query, token-streamed replies honouring `prefers-reduced-motion`). New `Slider`/`Tabs` primitives + `CorpusMultiSelect`/`GuardBadge`/`CitedText` shared components; identity body-carried on `/v1/query`·`/v1/retrieve`·`/v1/feedback`, header-driven elsewhere. Gates: tsc / eslint / **46 vitest** (12 new) / `next build` (17 static routes); browser-verified light+dark, all three pages + the trace deep-link. [reference/admin-ui.md](docs/reference/admin-ui.md) - **v1.0.1** ✅ *(patch)* — **production serve path fixed end-to-end.** The image's default CMD (a Phase-0 placeholder that printed the version and exited) now serves via the new config-driven **`rag_gateway.serve:create_app`** uvicorn-factory entrypoint: `RAG_CONFIG_PATH` set → `build_app_from_config(load(path))`, unset → noop `build_app()`, set-but-broken → **fail fast** (`ConfigError`), never a silent noop fallback. The image **build** itself was also broken — the full `uv sync` needs every workspace member on disk but `sdks/python` was never copied (`Distribution not found`); both stages now copy it. New public **`GET /readyz`** backs the Helm chart's readiness probe (liveness stays `/healthz`). The chart's ConfigMap previously rendered an *invalid* `rag.yaml` (`env:/log_level:/service:` against the strict `extra='forbid'` schema → guaranteed CrashLoop); it now renders a minimal valid document with a **`config.ragYaml`** passthrough for full platform configs (`${VAR}` env interpolation keeps secrets out of the ConfigMap), chart `0.2.0` / `appVersion` `1.0.1` (the old `0.1.0` appVersion pointed the default image tag at a tag that never existed), `values.dev.yaml` `env: development` → `local`. Docs: image tags are published *without* the `v` prefix — `:v1.0.0`-style pulls in README/release-notes never matched. [release notes](docs/release-notes/v1.0.1.md) [#185](https://github.com/officialCodeWork/AgentContextOS/pull/185) - **7.10** ✅ **GA cutover — v1.0.0** 🎉 — the close of an 8-phase, 84-step build. Cut **`v1.0.0`** with **Semantic Versioning** over the drift-gated public contracts (REST/OpenAPI · gRPC proto · `rag.yaml` · SDKs · `ragctl`) + a deprecation policy ([ADR-0052](docs/adr/ADR-0052-ga-cutover.md)); the `rag-platform` meta bumped to 1.0.0. GA **release notes** ([v1.0.0](docs/release-notes/v1.0.0.md)) + a **GA readiness checklist** ([ga-readiness-checklist](docs/ga/ga-readiness-checklist.md)) that maps every Phase 7 exit gate to a CI gate / in-repo artifact, with inherently-external items (external pentest, SOC 2 audit, registry first-publish, signed customers, on-call staffing, launch assets) marked **(external)** — the same in-repo-machine vs external-deliverable split used across Phase 7. The `v1.0.0` tag fans out to every channel via `docker.yml` + `release.yml` + `release-airgap.yml`. - **7.9** ✅ Billing & metering — per-tenant usage accounting, **mirroring the 5.6c cost pattern** (dataclasses in `rag-observability`, the gateway wraps a Pydantic response → **no `dist/schemas` churn**). New `rag_observability.billing`: a `UsageMeter` (observe-only per-tenant, per-dimension counters: queries / docs / storage / reranker / LLM tokens) fed O(1) from `record_request_usage` independent of quotas; `GET /v1/billing/usage` serves the per-tenant dashboard; pure `generate_invoice` + `PricingModel`/`load_pricing` price an invoice **from `marketplace/pricing.yaml`** (a price change is config) and **reconcile with metered usage ±0.5% by construction**; a `BillingProvider` Protocol seam for Stripe / marketplace metering (degrade-open). Inert by default (`cfg.billing`); `ragctl billing` drives meter→invoice→reconcile; **no governed SPI call** (policy-coverage linter unchanged); only `dist/openapi` + `dist/rag.schema` regenerate. Deferred: the concrete Stripe/marketplace adapters, the docs/reranker/storage feed points, an admin usage card. [ADR-0051](docs/adr/ADR-0051-billing-metering.md) [#182](https://github.com/officialCodeWork/AgentContextOS/pull/182) diff --git a/apps/admin-ui/src/app/chat/page.tsx b/apps/admin-ui/src/app/chat/page.tsx new file mode 100644 index 0000000..7c067a9 --- /dev/null +++ b/apps/admin-ui/src/app/chat/page.tsx @@ -0,0 +1,701 @@ +"use client"; + +import { Binary, MessageSquare, Send } from "lucide-react"; +import { useCallback, useEffect, useRef, useState } from "react"; + +import { useApp } from "@/components/providers"; +import { + CitedText, + CorpusMultiSelect, + DataPanel, + GuardBadge, + PageHeader, + useResolvedState, +} from "@/components/shared"; +import { Sparkline } from "@/components/ui/charts"; +import { + Badge, + Button, + Card, + CardBar, + CopyButton, + DemoDataBadge, + EmptyState, + ErrorState, + Field, + Input, + Label, + Mono, + Select, + Skeleton, + Slider, + Switch, + TBody, + TD, + TH, + THead, + TR, + Table, + Tabs, + Textarea, +} from "@/components/ui/primitives"; +import { + GatewayError, + chatComplete, + createEmbeddings, + fetchCorpora, + fetchModels, + streamChat, + type ChatRequestInput, +} from "@/lib/api"; +import { cn } from "@/lib/cn"; +import { GATEWAY_URL } from "@/lib/config"; +import { + CORPORA, + embeddingToBase64, + seedChatReply, + seedEmbedding, + seedModels, +} from "@/lib/mock"; +import type { + ChatUsage, + Citation, + Corpus, + DataSource, + EmbeddingsUsage, + ModelInfo, + RagAnnotations, +} from "@/lib/types"; +import { useLive } from "@/lib/use-live"; + +function prefersReducedMotion(): boolean { + return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; +} + +const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +// --------------------------------------------------------------------------- +// Chat tab +// --------------------------------------------------------------------------- +interface ThreadMessage { + id: number; + role: "user" | "assistant"; + content: string; + streaming: boolean; + rag: RagAnnotations | null; + usage: ChatUsage | null; + finish_reason: string | null; + error: string | null; +} + +let msgSeq = 0; + +const STARTERS = ["What is our incident escalation policy?", "How do severities map to response SLAs?"]; + +function FinishReasonBadge({ reason }: { reason: string | null }) { + if (!reason) return null; + const variant = reason === "content_filter" ? "warning" : reason === "error" ? "error" : "outline"; + return {reason}; +} + +function RagSummary({ msg }: { msg: ThreadMessage }) { + if (msg.error) return null; + if (msg.rag && !msg.rag.enabled) { + return

RAG off — model-only answer

; + } + return ( +
+ {msg.rag?.shape && {msg.rag.shape}} + {msg.rag && {msg.rag.chunks_n} chunks} + {msg.rag?.citations.map((c: Citation, i: number) => ( + + [{i + 1}] + + ))} + {msg.rag?.bm25_fallback && bm25 fallback} + {msg.rag && } + + {msg.usage && ( + + {msg.usage.prompt_tokens}+{msg.usage.completion_tokens}={msg.usage.total_tokens} tok + + )} +
+ ); +} + +function ChatTab({ models }: { models: ModelInfo[] }) { + const { tenant } = useApp(); + const { rows: corpora } = useLive( + (tid) => CORPORA.filter((c) => c.tenant_id === tid), + fetchCorpora, + ); + + const [messages, setMessages] = useState([]); + const [composer, setComposer] = useState(""); + const [busy, setBusy] = useState(false); + + // request controls (ChatCompletionRequest + RagOptions) + const [model, setModel] = useState("rag-gateway"); + const [temperature, setTemperature] = useState(0); + const [maxTokens, setMaxTokens] = useState(1024); + const [stream, setStream] = useState(true); + const [ragEnabled, setRagEnabled] = useState(true); + const [ragCorpusIds, setRagCorpusIds] = useState([]); + const [ragTopK, setRagTopK] = useState(10); + const [ragRerank, setRagRerank] = useState(true); + const [ragPack, setRagPack] = useState(true); + + const threadRef = useRef(null); + const streamTimer = useRef | null>(null); + + useEffect(() => { + setMessages([]); + setRagCorpusIds([]); + }, [tenant.id]); + useEffect( + () => () => { + if (streamTimer.current) clearInterval(streamTimer.current); + }, + [], + ); + useEffect(() => { + const el = threadRef.current; + if (el) el.scrollTop = el.scrollHeight; + }, [messages]); + + function patchMsg(id: number, fields: Partial) { + setMessages((cur) => cur.map((m) => (m.id === id ? { ...m, ...fields } : m))); + } + + const send = useCallback( + async (text: string) => { + const q = text.trim(); + if (!q || busy) return; + setComposer(""); + setBusy(true); + const history = [...messages, { role: "user" as const, content: q }]; + setMessages((cur) => [ + ...cur, + { id: ++msgSeq, role: "user", content: q, streaming: false, rag: null, usage: null, finish_reason: null, error: null }, + ]); + const asstId = ++msgSeq; + setMessages((cur) => [ + ...cur, + { id: asstId, role: "assistant", content: "", streaming: true, rag: null, usage: null, finish_reason: null, error: null }, + ]); + + const input: ChatRequestInput = { + model, + messages: history.map((m) => ({ role: m.role, content: m.content })), + temperature, + max_tokens: maxTokens, + rag: { enabled: ragEnabled, corpus_ids: ragCorpusIds, top_k: ragTopK, rerank: ragRerank, pack: ragPack }, + }; + + if (!GATEWAY_URL) { + await delay(420); + const reply = seedChatReply(tenant.id, { enabled: ragEnabled, top_k: ragTopK }); + const finish = () => { + patchMsg(asstId, { + content: reply.text, + streaming: false, + rag: reply.rag, + usage: reply.usage, + finish_reason: reply.finish_reason, + }); + setBusy(false); + }; + if (!stream || prefersReducedMotion()) { + await delay(300); + finish(); + return; + } + const words = reply.text.split(" "); + let i = 0; + streamTimer.current = setInterval(() => { + i += 1; + patchMsg(asstId, { content: words.slice(0, i).join(" ") }); + if (i >= words.length) { + if (streamTimer.current) clearInterval(streamTimer.current); + streamTimer.current = null; + finish(); + } + }, 24); + return; + } + + try { + const final = stream + ? await streamChat(tenant.id, input, (delta) => + setMessages((cur) => cur.map((m) => (m.id === asstId ? { ...m, content: m.content + delta } : m))), + ) + : await chatComplete(tenant.id, input); + patchMsg(asstId, { + content: final.content, + streaming: false, + rag: final.rag, + usage: final.usage, + finish_reason: final.finish_reason, + }); + } catch (err) { + patchMsg(asstId, { + streaming: false, + finish_reason: "error", + error: err instanceof GatewayError ? err.message : String(err), + }); + } finally { + setBusy(false); + } + }, + [busy, messages, model, temperature, maxTokens, stream, ragEnabled, ragCorpusIds, ragTopK, ragRerank, ragPack, tenant.id], + ); + + return ( +
+ {/* thread */} + +
+ {messages.length === 0 ? ( +
+
+ +
+

Ask a question grounded in your corpora

+

+ Retrieval runs over the last user turn and the context is injected before the LLM call. +

+
+ {STARTERS.map((s) => ( + + ))} +
+
+ ) : ( + messages.map((m) => ( +
+
+
+ {m.error ? ( + + {`{ "error": { "message": "${m.error}", "type": "server_error" } }`} + + ) : ( + <> + + {m.streaming && ( + + )} + + )} +
+ {m.role === "assistant" && !m.streaming && } +
+
+ )) + )} +
+
+