Appearance
Knowledge Indexing Process
How documentation in this repo becomes Emily's searchable knowledge base.
Overview
┌─────────────────┐ ┌─────────────────┐ ┌──────────────────────┐
│ Markdown │ │ Extraction │ │ upvendo-backend │
│ Documentation │ ──▶ │ (extract.js) │ ──▶ │ KB ingest → Postgres│
└─────────────────┘ └─────────────────┘ └──────────────────────┘
│
▼
┌─────────────────┐
│ Emily │
│ Assistant │
└─────────────────┘The backend owns embedding and search (upvendo-backend docs/kb-search.md). This repo only extracts Markdown into chunks and sends them. Azure AI Search was shut down on 2026-09-28; azure-export/ keeps the old index definitions as a historical snapshot.
Step 1: Documentation
All knowledge starts as markdown files in this repository:
upvendo-kb/
├── shared/ # → audience `shared` (developer/ excluded)
│ ├── features/ # Feature documentation
│ ├── fields/ # Field reference
│ ├── guides/ # How-to, FAQ, glossary
│ ├── integrations/ # Per-provider docs
│ ├── relations/ # Cross-system relations
│ ├── troubleshooting/# Issue guides
│ └── developer/ # NOT indexed — read directly by Upforge / Claude Code
├── merchant/ # → audience `merchant`
├── reseller/ # → audience `reseller`
└── docs/ # → audience `shared` (this file included)Documentation Standards
- Use consistent structure (see templates)
- Include all required sections
- Write for clarity (a child should understand)
- Provide examples
- Document customer impact
Step 2: Extraction
bash
npm run extract # node scripts/extract.jsextract.js walks KNOWLEDGE_STRUCTURE (the shared/, merchant/, reseller/, global-admin/ contexts) plus ROOT_DIRS = ['docs'], parses each Markdown file into structured JSON entries, and tags every entry with its context. category is set to the source folder name (e.g. features, onboarding), not a fixed enum. Output: .knowledge/entries.json (plus one entries-{context}.json per context).
There is no content-generation step and no translation step.
Step 3: Ingest to the backend Postgres KB
bash
node scripts/ingest-to-backend.js --env test|prod --full # full sync
node scripts/ingest-to-backend.js --env prod --files "a.md" --deleted "b.md" # incremental
# add --dry-run to print the batch plan and validate every chunk without callingReads .knowledge/entries.json (the output of Step 2) and POSTs it to the ingest URL (/api/internal/kb/ingest on upvendo-backend, a Bearer ingest token). The backend embeds, so this step sends no vectors.
- Mapping: each extracted entry becomes a chunk (
toChunk()), with anaudiencederived from its context:shared,merchant,reseller, oradminforglobal-admin; anything else falls back toshared. The backend 422s on any key outsideALLOWED_KEYS. content_hash: sha256 of the JSON of every other chunk field in a fixed order. The backend skips an unchanged hash entirely (no re-embed, no rewrite), so every stored field must be in it.- Batches: ≤100 chunks, a file's chunks kept together. Incremental batches name their files in
delete_source_files, which drops sections a file no longer has;--deletedfiles go in extra chunk-less requests.--fullends with a chunk-lessreplace_all_for_envrequest whosereplace_beforeis the first response'ssynced_at, deleting every row the run did not send. A failed batch aborts the run before that final prune. - Retries: 429, 5xx, network errors and timeouts, up to 5 times with backoff. Any other 4xx fails the run.
- Secrets: the ingest URL and the ingest token, set per GitHub environment (
testing,production). Without them the script exits non-zero, so a misconfigured environment fails the workflow instead of silently skipping the sync.--dry-runneeds neither.
{env} is test for the testing branch and prod for production.
Automation
Two workflows run on push to testing or production:
.github/workflows/update-knowledge.yml(any*.mdexceptREADME.md): detect added/modified and deleted files → extract → ingest (--files+--deleted) → invalidate the proxy setup-steps cache → summary → Slack..github/workflows/index-knowledge.yml(shared/,merchant/,reseller/,global-admin/,docs/): determine changed files → determine env (test/prod) → extract → ingest (--files) → invalidate cache → summary → Slack.
Both skip when the push touches no indexable file (e.g. shared/developer/** only), using extract.js --filter-indexable.
Manual Trigger
Run a workflow from the GitHub Actions UI via workflow_dispatch: update-knowledge.yml takes mode (incremental / full) and environment; index-knowledge.yml takes full_reindex. A full sync replaces every row for the environment, which also removes anything that should never have been indexed.
Verification
- Check the ingest step's log: it prints the request plan and the backend's per-batch
upserted/unchanged/embedded/deletedtotals. - Ask Emily questions about the indexed content:
- "How do I set up order capacity?"
- "What does the time slot duration field do?"
- "Why are customers not seeing available times?"
Troubleshooting
Entries Not Appearing
- Check the ingest step's log for a 4xx (bad chunk) or exhausted retries
- Run
node scripts/ingest-to-backend.js --env test --full --dry-runlocally afternpm run extract -- --fullto validate every chunk - Look for
⚠️ No entries extracted for: …— extract.js produced nothing for that file, so its existing rows were left untouched