# Knowledge base

An agent answers only from published help articles, citing each article and version, says it doesn't know when nothing matches and records the question as a gap, and drafts an article change that publishes, as a new version, only after approval; people edit articles and see the gaps on their own page.

- Registry name: `knowledge-base` (`@candor/knowledge-base`)
- Tier: Premium. Installing it needs a Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/knowledge-base

## Install

```bash
npx candor-ui add knowledge-base
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/knowledge-base`.
It brings in: badge, button, card-registry, chat, citation, composer, diff, freshness-stamp, input, know, locked, message, prompt-suggestions, prose-diff, relative-time, retrieval-inspector, textarea, thread, use-candor-chat, version-history.
npm packages: @ai-sdk/anthropic@^4.0.96, @ai-sdk/react@^4.0.166, ai@^7.0.162, drizzle-orm@^0.45.4, lucide-react@^1.67.0, postgres@^4.0.0, zod@^4.6.7.

It installs under your app's `app/` folder and runs with a server and an agent: set `ANTHROPIC_API_KEY`, and read the comment at the top of its `store.ts` for storage and auth. Its files:

- `app/knowledge-base/agent.ts`
- `app/knowledge-base/api/kb/route.ts`
- `app/knowledge-base/api/route.ts`
- `app/knowledge-base/articles/page.tsx`
- `app/knowledge-base/db/connect.ts`
- `app/knowledge-base/db/drizzle-repo.ts`
- `app/knowledge-base/db/schema.ts`
- `app/knowledge-base/db/seed.ts`
- `app/knowledge-base/page.tsx`
- `app/knowledge-base/repo.ts`
- `app/knowledge-base/retrieval.ts`
- `app/knowledge-base/store.ts`
- `app/knowledge-base/ui.tsx`

## <ArticlesPage>

People's side: read the articles, edit one (it publishes as a new version), confirm it's still right, and see the gaps.

No props.

## <Page>

No props.

## <ArticleDraft>

A drafted article change as a read-only word diff. Nothing here publishes it: that's the approval in the chat.

No props.

Also takes the props of `DraftCardData`.

## <ArticleLine>

One article's line: id and published version, owner, and when it was last confirmed right.

Props:

- `a: ArticleSummary`

## <KnowledgeBase>

The knowledge base: the published articles and open gaps beside a chat with an agent that answers only from those articles, cites them, records what it can't answer, and drafts changes that publish as a new version only after the person approves.

Props:

- `chat: KbChat`: The chat state from useCandorChat (AI SDK) or useClaudeAgent (Claude Agent SDK); the block works with either.
- `api: string`: The data route the articles and gaps are read from, e.g. "/knowledge-base/api/kb".
- `articlesHref: string`: Where people read and edit the articles.

## Functions

### connectKbDb

```ts
connectKbDb(url: string)
```

Open the block's database from a Postgres URL with the postgres-js driver. Swap the driver here (node-postgres, Neon's websocket Pool, …) if your app already uses another; it must support interactive transactions.

### drizzleKbRepo

```ts
drizzleKbRepo(db: KbDb)
```

### seedKb

```ts
seedKb(db: KbDb)
```

Load the demo's articles, their versions and the open gaps into the database. Articles and gaps get their ids from identity columns, in this order (KB-001…, G-001…), so the demo loads only into an empty knowledge base and leaves one that has articles alone. Run it once after migrating, e.g. from a script: `await seedKb(connectKbDb(process.env.DATABASE_URL!))`.

### demoKbData

```ts
demoKbData()
```

The demo's articles, versions and open gaps, for the in-memory store and for seeding a database (./db/seed.ts).

### memoryKbRepo

```ts
memoryKbRepo()
```

In this server process's memory: gone on restart and not shared between instances. Transactions run one at a time, and a transaction that throws is rolled back to the state it started from.

### termsOf

```ts
termsOf(text: string)
```

A question's or passage's search words: lowercase, without common words, and without a plural "s".

### retrieve

```ts
retrieve(question: string, docs: Doc[])
```

The passages that answer `question` best, highest score first (ties by article and paragraph). Empty when nothing scores.

### setKbRepo

```ts
setKbRepo(repo: KbRepo)
```

Use your own storage (or a database you opened yourself). Call it once, before the routes run.

### kbRepo

```ts
kbRepo()
```

The storage in use: whatever setKbRepo() was given, else Postgres when DATABASE_URL is set, else memory.

### viewerOf

```ts
viewerOf(request: Request)
```

Who is asking. Replace with your auth; the demo reads a cookie the articles page sets.

### searchArticles

```ts
searchArticles(question: string, by: string, searches: Searches)
```

Search the published articles for a question. When nothing is found the question is recorded as a gap here, in code, so an unanswered question is never lost to a model forgetting to record it.

### recordGap

```ts
recordGap(question: string, by: string)
```

A question the found passages didn't answer, recorded as a gap.

### giveAnswer

```ts
giveAnswer(input: { question: string; text: string; cites: { articleId: string; version: number }[] }, searches: Searches)
```

Check an answer's citations: each must be an article's published version that one search_articles call in this run found, in `searches`. The check rests on what search found, never on what the model says it searched for, and the answer card shows the question that search was for.

### readArticle

```ts
readArticle(articleId: string)
```

### draftArticle

```ts
draftArticle(input: { articleId?: string | undefined; title: string; text: string; reason: string; gapIds: string[] }, by: string)
```

A drafted change to an article, or a new article. Nothing is published yet.

### publishDraft

```ts
publishDraft(id: string, by: string)
```

Publish an approved draft as a new version, if the article hasn't changed since it was written. Older versions are kept.

### publishEdit

```ts
publishEdit(input: { articleId: string; baseVersion: number; title: string; text: string; note?: string | undefined }, by: string)
```

A person's own edit. It publishes as a new version at once, under the same rule: only against the version they edited.

### markReviewed

```ts
markReviewed(articleId: string, version: number, by: string)
```

The owner confirms the published version is still right. Only for the version they read.

### dismissGap

```ts
dismissGap(id: string, by: string)
```

A person decides a gap doesn't need an article.

### articleList

```ts
articleList()
```

The article list: each with its published version, owner, and whether it's due for review.

### articleView

```ts
articleView(id: string, viewer: string)
```

One article as a person sees it: its published text, every version, its open drafts, and why review is locked for them.

### resetStore

```ts
resetStore()
```

For tests: start from the seed data again. On a database this deletes the block's tables' rows.

### kbToolTitle

```ts
kbToolTitle(tool: string, input: unknown)
```

Plain-words titles for the block's tools, the same for both agent SDKs.

## Types

### KbDb

```ts
PgDatabase<PgQueryResultHKT, any>
```

### ArticleRecord

An article: who owns it, which version is published now, and when someone last confirmed it's still right.

```ts
{ id: string /** The title made comparable (lowercase, words only). Two articles can't share one. */ slug: string owner: string /** How often the owner should confirm it's still right. Past this it shows as due for review. */ reviewEveryDays: number /** The published version: the one the agent answers from. */ version: number reviewedAt: string reviewedBy: string createdAt: string }
```

### VersionRecord

One published version of an article. Versions are never changed or deleted.

```ts
{ articleId: string; version: number; title: string; text: string; by: string; note: string; createdAt: string; draftId?: string | undefined }
```

### GapRecord

A question the published articles couldn't answer.

```ts
{ id: string /** The question made comparable. Only one open gap per key. */ key: string question: string askedBy: string at: string state: "open" | "answered" | "dismissed" closedBy?: string | undefined closedAt?: string | undefined /** The version that answered it, when a published draft named this gap. */ answeredBy?: { articleId: string; version: number } | undefined }
```

### DraftRecord

```ts
{ id: string /** Null for a new article. */ articleId: string | null /** The version the draft was written against (0 for a new article); it publishes only while that's still current. */ baseVersion: number beforeTitle: string before: string title: string after: string reason: string /** The gaps it answers; they close when it publishes. */ gapIds: string[] by: string createdAt: string state: "dra
```

### NewArticleRecord

```ts
Omit<ArticleRecord, "id">
```

### NewGapRecord

```ts
Omit<GapRecord, "id" | "state" | "closedBy" | "closedAt" | "answeredBy">
```

### NewDraftRecord

```ts
Omit<DraftRecord, "id" | "state" | "publishedAs">
```

### ArticlePatch

```ts
Partial<Pick<ArticleRecord, "slug" | "version" | "reviewedAt" | "reviewedBy">>
```

### GapPatch

```ts
Pick<GapRecord, "state" | "closedBy" | "closedAt" | "answeredBy">
```

### DraftPatch

```ts
Partial<Pick<DraftRecord, "state" | "publishedAs">>
```

### Doc

```ts
{ articleId: string; version: number; title: string; text: string }
```

### Passage

```ts
{ id: string; articleId: string; version: number; title: string; excerpt: string; score: number; matched: string[] }
```

### Article

```ts
ArticleRecord
```

### Draft

```ts
DraftRecord
```

### Gap

```ts
GapRecord
```

### GapCardData

```ts
{ id: string; question: string; isNew: boolean; reason: string }
```

### Searches

What search_articles found during one agent run, kept by the route for that run. give_answer may cite only these, so the model can't make an article citable by choosing what to search for after the fact.

```ts
{ question: string; found: Passage[] }[]
```

### AnswerCardData

What the answer card shows. Built here, so the sources and what was retrieved are the service's word, not the model's.

```ts
{ question: string; text: string; sources: MessageSource[]; passages: RetrievedPassage[]; articles: KnowSource[] }
```

### DraftCardData

What the draft card shows: the change as a diff against the version it was written for.

```ts
Pick<DraftRecord, "id" | "articleId" | "baseVersion" | "beforeTitle" | "before" | "title" | "after" | "reason" | "gapIds">
```

### ArticleSummary

```ts
Awaited<ReturnType<typeof articleList>>[number]
```

### ArticleView

```ts
Awaited<ReturnType<typeof articleView>>
```

### KbSnapshot

What GET /api/kb returns (without an article).

```ts
{ viewer: string; articles: ArticleSummary[]; gaps: Gap[] }
```

### KbChat

What either adapter hook returns (useCandorChat or useClaudeAgent): the block works with both.

```ts
{ messages: MessageData[] composer: React.ComponentProps<typeof Composer> approvals: { id: string; tool: string; input?: unknown; title?: string | undefined; reason?: string | undefined }[] respond: (id: string, approved: boolean, reason?: string) => unknown error?: string | undefined }
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
