Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
8cf11b5
[Feat]: Per-kind model pools — getVlmModel / getEmbeddingModel (issue…
FireTable Jul 14, 2026
8a8c53d
[Feat]: KB JSON store (issue #13 v1, no DB)
FireTable Jul 14, 2026
e7b5e2a
[Feat]: KB PDF screenshot via mupdf (issue #13 v1)
FireTable Jul 14, 2026
03d99ab
[Feat]: kb_agent subgraph (screenshot → VLM → chunk-embed-store) (iss…
FireTable Jul 14, 2026
26e89d7
[Feat]: attachment-kb-injector wired into chat subgraph (issue #13 v1)
FireTable Jul 14, 2026
3c9e1bf
[Refactor]: kb_agent moved under backend/agent/ + injector lifted fro…
FireTable Jul 14, 2026
0c646a2
[Fix]: KB injector reads snake_case mime_type matching LangGraph SDK …
FireTable Jul 14, 2026
8aabcca
[Feat]: KB schema (kb_folder, kb_document, kb_chunk) + pgvector migra…
FireTable Jul 14, 2026
5ac1a67
[Feat]: KB resolve helpers (queries + LRU cache + extract + resolve)
FireTable Jul 14, 2026
f698bca
[Refactor]: trimMessagesForInvoke async + kb_ref resolve + R2 KB help…
FireTable Jul 14, 2026
d049edb
[Feat]: kbAgent subgraph v2 + RouterNode PDF routing + parent graph w…
FireTable Jul 14, 2026
b56da0f
[Chore]: delete v1 KB store + injector (replaced by v2)
FireTable Jul 14, 2026
44defec
[Fix]: KB v2 — strip file parts in router LLM call + base64 VLM data URL
FireTable Jul 15, 2026
95bae77
[Fix]: KB queries — detect 23505 in drizzle-wrapped PostgresError
FireTable Jul 15, 2026
b6445ef
[Test]: KB v2 — 5 test files (extract, cache, resolve, queries, kb-ag…
FireTable Jul 15, 2026
abb570e
chore(deps): add p-queue for KB ingest concurrency
FireTable Jul 15, 2026
a579edb
feat(kb): Settings → Knowledge Base tab rewrite
FireTable Jul 15, 2026
b9ae413
feat(backend): kbAgent pipeline — p-queue concurrency, R2 URL renderi…
FireTable Jul 15, 2026
6916643
feat(backend): PATCH /api/kb/folders/[id] for folder rename
FireTable Jul 15, 2026
5fd26bf
docs(api): document Knowledge Base endpoints
FireTable Jul 15, 2026
b3ee02c
feat(kb): Settings — edit folder + doc row vertical-center polish
FireTable Jul 15, 2026
9f62355
feat(settings): mobile tab label hiding via global CSS
FireTable Jul 15, 2026
c334888
refactor(backend): use SystemMessage + HumanMessage in kb vlmNode
FireTable Jul 15, 2026
773eec7
refactor(backend): force kb vlm output via withStructuredOutput
FireTable Jul 15, 2026
24b6a45
refactor(backend): drop renameThreadAgent END edge + clean router deb…
FireTable Jul 15, 2026
a2dac5c
refactor(backend): rename KB_VLM_PAGE_PROMPT to KB_OCR_PAGE_PROMPT
FireTable Jul 15, 2026
f8ba2d8
refactor(backend): rename vlm → ocr across provider registry + kb agent
FireTable Jul 15, 2026
6f9eae1
feat(backend): support multi-PDF kb ingest across all human messages
FireTable Jul 15, 2026
a003887
refactor(backend): drop last-only kb helpers and route by hasUnproces…
FireTable Jul 15, 2026
d73c210
feat(backend): per-model kind[] persisted across admin API, UI, and seed
FireTable Jul 15, 2026
6e6af7f
fix(admin): cap Models name column at 220px, rename Enabled → Status,…
FireTable Jul 15, 2026
54081c3
feat(backend): register kbAgent as standalone assistant + reprocess e…
FireTable Jul 15, 2026
6555027
fix(observability): count root-cause failures off the wire in failedC…
FireTable Jul 15, 2026
1541e7c
fix(observability): inline failed icon after the span name
FireTable Jul 15, 2026
d4611b2
style(observability): oxfmt panel.tsx
FireTable Jul 15, 2026
dd36bee
refactor(frontend): hoist buildUserMessageAttachments helper
FireTable Jul 15, 2026
e1427b5
feat(kb): migrate kb_chunk.embedding to vector(1024)
FireTable Jul 15, 2026
9f2689a
fix(kb): persist doc row through OCR/chunk failures
FireTable Jul 15, 2026
54d9368
feat(kb): surface kb_doc deep-links via state.values.kb_refs sidecar
FireTable Jul 16, 2026
c64a349
refactor(frontend): inline buildUserMessageAttachments back into atta…
FireTable Jul 16, 2026
62ba4ba
refactor(kb): stamp kb_ref as sibling field on file part
FireTable Jul 16, 2026
c36239f
fix(kb): resolve file-part-with-kb_ref-sibling in resolve layer
FireTable Jul 16, 2026
ef9b15a
refactor(frontend): drop kb_ref CompleteAttachment type, ride as file…
FireTable Jul 16, 2026
63fb013
feat(kb): deep-link KB tile to doc + dual-channel kb_ref marker
FireTable Jul 16, 2026
aa0b086
fix(kb): preserve already-stamped file parts in multi-turn rewrite
FireTable Jul 16, 2026
1df4e4b
refactor(kb): centralize KB concurrency constants in lib/constants.ts
FireTable Jul 16, 2026
c664838
feat(kb): persist OCR pages, split ingest pipeline, redesign doc detail
FireTable Jul 16, 2026
b82a0f5
refactor(kb): make generateChunkEmbed node self-sufficient via DB lookup
FireTable Jul 16, 2026
1343557
refactor(kb): keep generateChunkEmbed fire-and-forget, thread Runnabl…
FireTable Jul 16, 2026
64317ef
refactor(observability): log swallowed entity failures + doc missing …
FireTable Jul 16, 2026
00ff41d
fix(backend): skip rename dispatch when only stamped KB file parts re…
FireTable Jul 16, 2026
c7b55f1
fix(settings): stop KB detail dialog content from overflowing right edge
FireTable Jul 16, 2026
af4f356
fix(settings): cap KB dialog title width + hoist stripFileParts to li…
FireTable Jul 16, 2026
c945aa3
fix(settings): apply truncate via inner span so ellipsis fires in gri…
FireTable Jul 16, 2026
7072fac
refactor(backend): split per-turn transcript into separate user messages
FireTable Jul 16, 2026
ae4de43
feat(kb): feed native PDF text to OCR as reference + Pages tab refere…
FireTable Jul 16, 2026
e6755f0
feat(kb): @-mention KB docs + hybrid RRF search + ToolMessage context
FireTable Jul 17, 2026
c85fce9
refactor(kb): drop legacy standalone kb_ref part, ride as file siblin…
FireTable Jul 17, 2026
8f9d466
fix(backend): stop background_agent from overwriting state.messages
FireTable Jul 17, 2026
33f14ed
refactor(backend): move kb_ref resolve into threadSummarizeNode
FireTable Jul 17, 2026
da5047e
feat(kb): chunk parsing status decoupled from kb_document status
FireTable Jul 18, 2026
942b140
feat(kb): chunksOnly reprocess branch skips OCR, reuses pages[].markdown
FireTable Jul 18, 2026
2352945
chore(kb): switch chunk splitter to MarkdownTextSplitter, enable debu…
FireTable Jul 18, 2026
530d991
refactor(kb): rename DEBUG_BAIL_OUT to SKIP_CHUNK_TO_ENTRIES
FireTable Jul 18, 2026
c7854bf
feat(provider): add 'extract' kind and route KB entity extraction thr…
FireTable Jul 18, 2026
63cfb81
feat(kb): show spinner on chunk rollup while indexing, flip debug rai…
FireTable Jul 18, 2026
4439b91
feat(kb): upgrade chunk extraction to LightRAG (entities / relationsh…
FireTable Jul 18, 2026
13cf30c
refactor(frontend): split kb-view.tsx into per-concern files
FireTable Jul 18, 2026
8e8dce9
feat(kb): folder-level knowledge graph + sidebar user-menu entry
FireTable Jul 18, 2026
7722628
refactor(kb-view): extract shared KnowledgeGraph component with name-…
FireTable Jul 18, 2026
059d0d4
style(kb-graph): zoom-adaptive styling + thin base for dense docs
FireTable Jul 18, 2026
a1627d5
style(kb-view): tighten dialog loading skeletons to mirror real shape
FireTable Jul 18, 2026
30d5fb7
style(kb-view): tighten dialog loading skeletons + sync border with s…
FireTable Jul 18, 2026
767700c
refactor(kb-doc): loading skeleton mirrors loaded DialogHeader layout
FireTable Jul 18, 2026
45ac823
feat(kb-graph): hover tooltips + dangling-link filtering
FireTable Jul 18, 2026
515e13e
refactor(kb-agent): simplify chunk pipeline + new entity extract prompt
FireTable Jul 18, 2026
bad3725
fix(kb-graph): resolve hydration warning from DialogDescription / <p>
FireTable Jul 18, 2026
4795779
feat(kb): add retryFailed reprocess mode + dual doc/chunks status
FireTable Jul 19, 2026
7d9f1b6
feat(kb): track pendingChunks/parsingChunks + per-page status mirror
FireTable Jul 19, 2026
926a611
fix(kb-graph): stop force-graph flicker + extend doc-table polling pa…
FireTable Jul 19, 2026
ce6c7f8
fix(kb-view): keep polling after Reprocess/Upload/Delete dispatch
FireTable Jul 19, 2026
ebc22ef
feat(kb): persist selected folder in URL via ?folder=<id>
FireTable Jul 19, 2026
7cd7dd2
refactor(backend): extract resolveEntityAliasesForDoc helper from chu…
FireTable Jul 19, 2026
e9a5b3d
feat(kb-graph): graphRAG-native node color from neighbor signature + …
FireTable Jul 19, 2026
998c2df
fix(kb): give No Chunks badge a visible background
FireTable Jul 19, 2026
fb6993b
fix(kb-graph): thinner links + smaller arrowheads + light label halo
FireTable Jul 19, 2026
4513842
fix(kb-view): unify Pages + Chunks status badge style
FireTable Jul 19, 2026
6d2b68f
fix(backend): enable strict jsonSchema on all 5 LLM structured-output…
FireTable Jul 19, 2026
216fdc3
feat(kb): retryFailedChunks reprocess mode — UPDATE in place, no OCR
FireTable Jul 19, 2026
a5ddeed
fix(kb-view): keep polling when chunks lag pages + slow poll cadence
FireTable Jul 19, 2026
f1b4e20
feat(kb): vector leg in search tools + folder-scoped doc list API
FireTable Jul 19, 2026
fac036f
feat(kb-view): live polling indicator + extract KB_POLL_INTERVAL_MS
FireTable Jul 19, 2026
5a00d2f
feat(kb): Reranker + @-mention optimization + chunk list UI
FireTable Jul 19, 2026
b6d4de6
fix(kb): rename @-mention directive {name=id} to {id=} + use doc.docId
FireTable Jul 19, 2026
15d3b88
feat(kb): Reranker env wiring + iterative search + KB v3 docs
FireTable Jul 19, 2026
6bb94f3
refactor(backend): rename ALL_TOOLS to CHAT_TOOLS
FireTable Jul 19, 2026
255a97b
refactor(kb): drop search_graph tool (duplicate of search_kb)
FireTable Jul 19, 2026
a53c9e4
feat(kb): group list_documents by folder with collapsible UI
FireTable Jul 19, 2026
105b620
style(kb): swap chevron unicode for lucide ChevronRight icon
FireTable Jul 19, 2026
ecf55e0
style(kb): move score badge after leg badges in search_kb card
FireTable Jul 19, 2026
89c38a2
style(kb): match Show more/less button style to search_kb card
FireTable Jul 19, 2026
69b8813
fix(kb): keep Show more/less button anchored at bottom of folder
FireTable Jul 19, 2026
b9a245f
refactor(kb): fold kb_context_retrieval into search_kb
FireTable Jul 19, 2026
447fdad
refactor(kb): scope resolveKbMentions to the 0-chunk fallback only
FireTable Jul 19, 2026
3a12ff3
chore(kb): drop <mentioned-documents> from system prompt + stale comment
FireTable Jul 19, 2026
2fc37dc
refactor(kb): LLM-driven @-mention flow + search-friendly query
FireTable Jul 19, 2026
5ebc651
feat(kb): extend tag leg + transparent scope-dump fallback
FireTable Jul 19, 2026
1a23662
fix(kb-view): drop redundant onRefresh after doc delete
FireTable Jul 19, 2026
c7dd7b1
feat(kb): per-doc observability run history
FireTable Jul 20, 2026
1ee44ae
feat(landing): add dedicated Knowledge Base section + bento card
FireTable Jul 20, 2026
0b5c2e7
fix(kb-agent): await chunk+embed for standalone Settings path
FireTable Jul 20, 2026
cf14428
style(brand-mark): drop chat-bubble icon next to app name
FireTable Jul 20, 2026
7ceadf0
feat(landing): merge KB into Memory card + KB explainer in How-it-works
FireTable Jul 20, 2026
426a832
style(cta): drop section border + card ring for flatter look
FireTable Jul 20, 2026
5523e52
style(landing): mute sparkle icons in kb-explainer + drop redundant w…
FireTable Jul 20, 2026
0b17704
fix: unify kb_observability insert across modes + dialog description …
FireTable Jul 20, 2026
72f48d2
fix(kb): simplify waitForChunks decision + accept top-level root chains
FireTable Jul 20, 2026
7edea5a
docs(kb): sync providers + memory topology + KB doc paths
FireTable Jul 20, 2026
d7744c3
build(infra): swap postgres image to pgvector/pgvector for KB v3
FireTable Jul 20, 2026
d94a16f
fix(kb): 3 review blockers — reprocess tx order, search vector leg, e…
FireTable Jul 20, 2026
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
29 changes: 28 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,31 @@ LLM_KEY_ENCRYPTION_KEY=
# (case-insensitive), their role_id is set to 'admin'. Idempotent — leave
# set forever; only the FIRST signup with this email is promoted. To add
# a second admin later, use the admin UI role management tab.
INITIAL_ADMIN_EMAIL=
INITIAL_ADMIN_EMAIL=

# KB retrieval knobs (issue #13 v3). All server-only — backend tool
# surface and mention resolver read these via lib/kb/env.ts. Defaults
# reflect the community survey in `.claude/13-kb-v3.md`:
# LangChain EnsembleRetriever / LlamaIndex QueryFusionRetriever /
# Haystack DocumentJoiner — per-leg 50, fused 5–10; Mastra = 3;
# Mastra + Vercel AI SDK RAG chunks = 512–1024 tokens.
# KB_MENTION_TOPK_DEFAULT — chunks per single @-mention. 5.
# KB_MENTION_TOPK_MAX — per-mention upper bound when user overrides. 20.
# KB_MENTION_TOKEN_BUDGET — total token cap across multi-mention turns.
# Per-mention topK is rebudgeted as
# ceil(BUDGET / (CHUNK_MAX_CHARS/4 * mentions)).
# 8192.
# KB_HYBRID_TOPK_DEFAULT — fused topK for the search_kb tool. 8.
# KB_HYBRID_TOPK_MAX — fused topK upper bound for the tool. 20.
# KB_CHUNK_MAX_CHARS — per-chunk truncation before stuffing into
# the LLM prompt; ~512 tokens at 4 chars/token.
# 2000.
# KB_RERANK_MIN_SCORE — minimum rerank relevance score threshold for filtering.
# Chunks below this score are filtered out. 0.4.
KB_MENTION_TOPK_DEFAULT=5
KB_MENTION_TOPK_MAX=20
KB_MENTION_TOKEN_BUDGET=8192
KB_HYBRID_TOPK_DEFAULT=8
KB_HYBRID_TOPK_MAX=20
KB_CHUNK_MAX_CHARS=2000
KB_RERANK_MIN_SCORE=0.4
16 changes: 14 additions & 2 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,13 @@ jobs:
# to bring the schema up — the build-time DB can be empty otherwise.
services:
postgres:
image: postgres:16-alpine
# pgvector/pgvector:pg16 (Debian-based, FROM postgres:16) ships
# the vector extension pre-installed — required by migration
# 0005_*.sql (CREATE EXTENSION vector). Stock postgres:16-alpine
# does not bundle it. Wire-format (POSTGRES_*, PGDATA, init
# scripts) is unchanged from upstream so all 6 build/test steps
# keep working without further edits.
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
Expand Down Expand Up @@ -109,7 +115,13 @@ jobs:
timeout-minutes: 10
services:
postgres:
image: postgres:16-alpine
# pgvector/pgvector:pg16 (Debian-based, FROM postgres:16) ships
# the vector extension pre-installed — required by migration
# 0005_*.sql (CREATE EXTENSION vector). Stock postgres:16-alpine
# does not bundle it. Wire-format (POSTGRES_*, PGDATA, init
# scripts) is unchanged from upstream so all 6 build/test steps
# keep working without further edits.
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# v1 KB JSON store — temporary local storage, replaced by DB tables in v2
/.kb-store/

# env files (can opt-in for committing if needed)
.env*
!.env.test
Expand Down
37 changes: 19 additions & 18 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,24 @@ Guidance for Claude Code in this repo. Features, layout, env vars, tech stack

## Docs index

| Topic | File |
| ----------------------------------------------- | ----------------------- |
| Features, quick-start, layout, env vars, deps | `README.md` |
| Marketing landing page sections + assets | `docs/LANDING.md` |
| Every HTTP endpoint under `app/api/` | `docs/APIS.md` |
| Memory + thread-summarize design | `docs/MEMORY.md` |
| Observability panel design + retention | `docs/OBSERVABILITY.md` |
| LangGraph tool inventory + frontend card wiring | `docs/TOOLS.md` |
| Interrupt-driven tool flow contract | `docs/INTERRUPT.md` |
| Attachments backing (R2 + presign) design | `docs/ATTACHMENTS.md` |
| Auth setup, OAuth, troubleshooting | `docs/AUTH.md` |
| DB schema, ownership, indexes | `docs/DB.md` |
| Provider registry (round-robin + withFallbacks) | `docs/PROVIDERS.md` |
| Credit / LLM-call quota system | `docs/CREDIT.md` |
| Admin UI + endpoints for providers/users/roles | `docs/ADMIN.md` |
| Production deploys (Docker, env, Caddy) | `docs/DEPLOY.md` |
| CI/CD, Docker, deploys | `docs/CI.md` |
| Topic | File |
| ------------------------------------------------ | ------------------------ |
| Features, quick-start, layout, env vars, deps | `README.md` |
| Marketing landing page sections + assets | `docs/LANDING.md` |
| Every HTTP endpoint under `app/api/` | `docs/APIS.md` |
| Memory + thread-summarize design | `docs/MEMORY.md` |
| Knowledge base, ingestion, hybrid search, Rerank | `docs/KNOWLEDGE_BASE.md` |
| Observability panel design + retention | `docs/OBSERVABILITY.md` |
| LangGraph tool inventory + frontend card wiring | `docs/TOOLS.md` |
| Interrupt-driven tool flow contract | `docs/INTERRUPT.md` |
| Attachments backing (R2 + presign) design | `docs/ATTACHMENTS.md` |
| Auth setup, OAuth, troubleshooting | `docs/AUTH.md` |
| DB schema, ownership, indexes | `docs/DB.md` |
| Provider registry (round-robin + withFallbacks) | `docs/PROVIDERS.md` |
| Credit / LLM-call quota system | `docs/CREDIT.md` |
| Admin UI + endpoints for providers/users/roles | `docs/ADMIN.md` |
| Production deploys (Docker, env, Caddy) | `docs/DEPLOY.md` |
| CI/CD, Docker, deploys | `docs/CI.md` |

API changes update `docs/APIS.md` in the same commit (rule 1). Tool changes update `docs/TOOLS.md` (rule 10).

Expand Down Expand Up @@ -75,7 +76,7 @@ Non-negotiable. Every change.

## Things to know before editing

- Graph id `agent` is in `langgraph.json`, `LANGGRAPH_ASSISTANT_ID` (`.env.example`, surfaced to client via `window.__CONFIG__` — see rule #12), and `unstable_createLangGraphStream({ assistantId })`. Keep aligned.
- Graph id `agent` is in `langgraph.json`, `LANGGRAPH_ASSISTANT_ID` (`.env.example`, surfaced to client via `window.__CONFIG__` — see rule #12), and `unstable_createLangGraphStream({ assistantId })`. Keep aligned. The `kbAgent` assistant id (KB ingestion, `langgraph.json` + `lib/kb/ingest.ts:fireIngestionRun` dispatch) follows the same sync rule — there is no env-driven alias for it, but renaming it requires updating `langgraph.json` AND `fireIngestionRun` together.
- `app/api/[..._path]/route.ts` proxy uses `runtime = "nodejs"` (was edge) — `withAuth` needs Node `net` for Postgres session reads.
- `components.json` declares a `@assistant-ui` registry at `https://r.assistant-ui.com/{name}.json` for `shadcn`-style component adds.
- `feat/*` branches: `git fetch origin main` and merge if main moved before committing — see [[feature-branch-tracks-main]].
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ A self-hostable chat app (this repo: `langgraph-app`) that streams tokens from a
- **Crypto sub-agent**: price, NFT holdings (5-chain gallery via Alchemy Portfolio), and a simulated swap flow against an auto-funded Mock Coin balance.
- **Observability panel**: every LLM / Tool / Chain / Node span is captured by a `BaseCallbackHandler` and persisted to a `observability_spans` Postgres table. Each assistant message shows an icon button that opens a per-turn waterfall — duration, token usage, nested parent/child spans. The list endpoint is server-transformed (panel never carries the raw collector payload); per-row click lazy-loads the full span via a dedicated detail endpoint. See [docs/OBSERVABILITY.md](docs/OBSERVABILITY.md).
- **Chat attachments**: assistant-ui's `AttachmentAdapter` plus a presigned PUT to Cloudflare R2 — the browser uploads bytes directly to R2, nothing traverses Next.js. Lazy-register on missing env (mirrors DENO / ALCHEMY). See [docs/ATTACHMENTS.md](docs/ATTACHMENTS.md) for the key convention, messageId-deferred decision, and Content-Disposition XSS guard.
- **Knowledge Base & Hybrid Search**: PDF ingestion, page screenshot rendering, text chunking, and pgvector-backed three-leg RRF (Keyword, Vector, Tag) hybrid search combined with semantic Reranking (Cohere/Jina), `@` mention resolution (with automatic fallback to full markdown when chunks are not ready), dynamic budget scaling, and iterative search. See [docs/KNOWLEDGE_BASE.md](docs/KNOWLEDGE_BASE.md).
- **Per-LLM-call credit quota**: every successful call is metered against a UTC-aligned rolling-window cap read from `role.creditLimit` / `role.windowHours`. Enforcement lives at the `/api/[..._path]` proxy — when the cap is hit, the proxy synthesizes a `show_credit_card` SSE stream and the chat UI renders the credit-limit-reached card inline. The call log backs a per-user history (Settings → Credits) and an admin-managed rate config. See [docs/CREDIT.md](docs/CREDIT.md).
- **Admin console**: a single `/admin` page with three tabs — Providers (registry + encrypted API keys + per-model rates), Roles (credit caps + window length), Users (role assignment, ban with immediate session revoke, delete). The first admin is bootstrapped via `INITIAL_ADMIN_EMAIL`. See [docs/ADMIN.md](docs/ADMIN.md).

Expand Down Expand Up @@ -310,6 +311,7 @@ Test database stays isolated from dev — never put production-like data in `lan
- [`docs/APIS.md`](docs/APIS.md) — HTTP endpoint reference. Update whenever a route under `app/api/` changes.
- [`docs/LANDING.md`](docs/LANDING.md) — marketing landing at `/`: per-section file map, asset inventory, motion keyframes, route-group naming rule, frontend test layout.
- [`docs/MEMORY.md`](docs/MEMORY.md) — memory + thread-summarize design: dual-graph topology, `<memory>` + `<threads>` recall, `save_memory` RFC 6902 patches, store-anchored trigger window math, Memory tab UI, security stance.
- [`docs/KNOWLEDGE_BASE.md`](docs/KNOWLEDGE_BASE.md) — knowledge base design: ingestion pipeline, three-leg RRF hybrid search (Keyword, Vector, Tag), semantic Reranking and score filtering, `@` mention resolution (Meta vs Full Markdown mode), mention budgeting, and iterative search.
- [`docs/OBSERVABILITY.md`](docs/OBSERVABILITY.md) — observability panel design: callback handler wiring, `observability_spans` schema, server-side transform + aggregate, lazy-loaded row detail, security/redaction, retention config, and curl examples.
- [`docs/TOOLS.md`](docs/TOOLS.md) — LangGraph tool inventory and frontend card wiring. Update whenever a tool or card is added/removed/rerouted.
- [`docs/INTERRUPT.md`](docs/INTERRUPT.md) — interrupt-driven tool flows (ask_location, connect_wallet, place_crypto_order, get_order_status) — the two runtime paths the cards can take.
Expand Down
94 changes: 78 additions & 16 deletions app/admin/admin-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import {
Expand All @@ -32,11 +33,16 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
import { toast } from "sonner";

type PublicProviderApiKey = { name: string };
type ModelKind = "chat" | "ocr" | "embed" | "extract" | "rerank";
type PublicModel = {
name: string;
enabled: boolean;
inputPer1k: number;
outputPer1k: number;
// ponytail: server-side default ["chat"] means an older row may not
// carry the field at all. Fall back to chat at render time so the
// table shows a value for every model, regardless of seed-vs-new.
kind?: ModelKind[];
};
type PublicProviderRow = {
id: string;
Expand All @@ -56,6 +62,12 @@ type RoleRow = {
createdAt: string;
updatedAt: string;
};

// ponytail: module-scope constant so add-mode `initialKind ?? ["chat"]`
// keeps the same array reference across renders — `useEffect` dep
// comparison uses Object.is, and a fresh literal each render would loop
// forever (Next dev StrictMode catches it as "Maximum update depth").
const DEFAULT_KIND: ModelKind[] = ["chat"];
// ponytail: server-side join shape — user + role name inlined so the
// table can render "Admin" / "User" labels without a second round-trip.
// `roleName` is null when the FK points at a missing role (defensive —
Expand Down Expand Up @@ -360,8 +372,9 @@ function ModelsSection({ providerId, models }: { providerId: string; models: Pub
<table className="w-full text-sm">
<thead className="bg-muted/50 text-muted-foreground text-xs">
<tr>
<th className="px-3 py-2 text-left font-medium">Name</th>
<th className="px-3 py-2 text-left font-medium">Enabled</th>
<th className="w-[220px] px-3 py-2 text-left font-medium">Name</th>
<th className="px-3 py-2 text-left font-medium">Status</th>
<th className="px-3 py-2 text-left font-medium">Kind</th>
<th className="px-3 py-2 text-right font-medium">Input / 1k</th>
<th className="px-3 py-2 text-right font-medium">Output / 1k</th>
<th className="px-3 py-2 text-right font-medium" />
Expand All @@ -370,19 +383,28 @@ function ModelsSection({ providerId, models }: { providerId: string; models: Pub
<tbody>
{models.length === 0 ? (
<tr>
<td colSpan={5} className="text-muted-foreground px-3 py-3 text-center text-xs">
<td colSpan={6} className="text-muted-foreground px-3 py-3 text-center text-xs">
No models configured.
</td>
</tr>
) : (
models.map((m) => (
<tr key={m.name} className="border-t">
<td className="px-3 py-2 font-mono text-xs">{m.name}</td>
<td className="w-[220px] px-3 py-2 font-mono text-xs">{m.name}</td>
<td className="px-3 py-2">
<Badge variant={m.enabled ? "success" : "muted"}>
{m.enabled ? "Enabled" : "Disabled"}
</Badge>
</td>
<td className="px-3 py-2">
<div className="flex flex-wrap gap-1">
{(m.kind ?? ["chat"]).map((k) => (
<Badge key={k} variant="secondary">
{k.charAt(0).toUpperCase() + k.slice(1)}
</Badge>
))}
</div>
</td>
<td className="px-3 py-2 text-right font-mono text-xs">{m.inputPer1k}</td>
<td className="px-3 py-2 text-right font-mono text-xs">{m.outputPer1k}</td>
<td className="px-3 py-2 text-right">
Expand Down Expand Up @@ -511,19 +533,35 @@ function ModelDialog(
const initialEnabled = isEdit ? props.model.enabled : true;
const initialIn = isEdit ? String(props.model.inputPer1k) : "0.001";
const initialOut = isEdit ? String(props.model.outputPer1k) : "0.002";
// ponytail: backend defaults kind to ["chat"] on POST when omitted,
// so the add-mode local seed mirrors the server contract and we don't
// need to send `kind` in the body. Edit-mode seeds from existing row.
const initialKind: ModelKind[] = isEdit ? (props.model.kind ?? DEFAULT_KIND) : DEFAULT_KIND;

const [name, setName] = useState(initialName);
const [enabled, setEnabled] = useState(initialEnabled);
const [inputPer1k, setInputPer1k] = useState(initialIn);
const [outputPer1k, setOutputPer1k] = useState(initialOut);
const [kind, setKind] = useState<ModelKind[]>(initialKind);
const [saving, start] = useTransition();

useEffect(() => {
setName(initialName);
setEnabled(initialEnabled);
setInputPer1k(initialIn);
setOutputPer1k(initialOut);
}, [initialName, initialEnabled, initialIn, initialOut]);
setKind(initialKind);
}, [initialName, initialEnabled, initialIn, initialOut, initialKind]);

const toggleKind = (k: ModelKind, checked: boolean) => {
setKind((prev) => {
const next = checked ? Array.from(new Set([...prev, k])) : prev.filter((x) => x !== k);
// ponytail: at least one kind must stay selected — unchecking the
// last box is a no-op so the user can never save a model with
// kind = [] (which the API rejects with 400 anyway).
return next.length === 0 ? prev : next;
});
};

const save = () => {
if (!name.trim()) {
Expand All @@ -541,8 +579,8 @@ function ModelDialog(
: `/api/admin/providers/${encodeURIComponent(props.providerId)}/models`;
const method = isEdit ? "PATCH" : "POST";
const body = isEdit
? { name: name.trim(), enabled, inputPer1k: inp, outputPer1k: out }
: { name: name.trim(), enabled, inputPer1k: inp, outputPer1k: out };
? { name: name.trim(), enabled, inputPer1k: inp, outputPer1k: out, kind }
: { name: name.trim(), enabled, inputPer1k: inp, outputPer1k: out, kind };
start(async () => {
const r = await jsonFetch(path, { method, body: JSON.stringify(body) });
if (!r.ok) {
Expand Down Expand Up @@ -580,15 +618,6 @@ function ModelDialog(
className="font-mono"
/>
</label>
<label className="flex items-center justify-between gap-3">
<span className="text-sm font-medium">Enabled</span>
<Switch
checked={enabled}
onCheckedChange={setEnabled}
disabled={saving || props.pending}
aria-label="Model enabled"
/>
</label>
<label className="flex flex-col gap-1.5">
<span className="text-sm font-medium">Input / 1k</span>
<Input
Expand All @@ -611,6 +640,39 @@ function ModelDialog(
disabled={saving || props.pending}
/>
</label>
<label className="flex items-center justify-between gap-3">
<span className="text-sm font-medium">Enabled</span>
<Switch
checked={enabled}
onCheckedChange={setEnabled}
disabled={saving || props.pending}
aria-label="Model enabled"
/>
</label>
<div className="flex flex-col gap-1.5">
<span className="text-sm font-medium">Kind</span>
<div className="flex flex-wrap gap-3">
{(["chat", "ocr", "embed", "extract", "rerank"] as const).map((k) => (
<label key={k} className="flex items-center gap-2 text-sm">
<Checkbox
checked={kind.includes(k)}
onCheckedChange={(c) => toggleKind(k, c === true)}
disabled={saving || props.pending}
aria-label={`kind ${k}`}
/>
{/* ponytail: title-case label for the dialog checkbox; the
wire-format enum stays lowercase (matches the JSONB value
and the registry's `m.kind ?? ["chat"]` contract). */}
<span>{k === "ocr" ? "OCR" : k.charAt(0).toUpperCase() + k.slice(1)}</span>
</label>
))}
</div>
<p className="text-muted-foreground text-xs">
At least one kind is required — chat for general inference, ocr for PDF vision, embed
for KB chunk vectors, extract for structured-output triples from chunks, rerank for
secondary retrieval reranking.
</p>
</div>
</div>
</FormDialog>
);
Expand Down
Loading
Loading