Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
36e7467
feat(search): per-assistant + global cross-encoder reranking, unbiase…
rajivml Jun 3, 2026
d41cf24
k8s: gpu-inference component (co-located rerank on a GPU node)
rajivml Jun 3, 2026
c3e2471
docs: branch design doc for reranking/recency/retrieval quality
rajivml Jun 3, 2026
b6e6ea7
feat(search): one-shot LLM relevance filter + source diversity + TEI …
rajivml Jun 7, 2026
d3f4207
feat(chat): per-conversation Rerank / Relevance toggles in the chat i…
rajivml Jun 7, 2026
f77f411
k8s: serve the reranker via CPU TEI (drop the GPU plan)
rajivml Jun 7, 2026
15f5281
test+docs: rerank/relevance/diversity tests + design-doc and dev-setu…
rajivml Jun 7, 2026
68568e0
k8s: bake bge-reranker-v2-m3 into our own ONNX TEI image
rajivml Jun 7, 2026
fc3c654
k8s(prod): bump backend vha-148, web vha-78 (rerank/relevance/diversity)
rajivml Jun 7, 2026
af74ee7
k8s: serve the reranker on GPU via the upstream TEI image
rajivml Jun 7, 2026
26c1600
k8s(prod): make source-diversity config explicit in env.properties
rajivml Jun 9, 2026
adb4dac
fix(auth): authorize valid X-API-Key requests under enforced auth (OIDC)
rajivml Jun 10, 2026
8f7a702
k8s(prod): bump backend vha-149 (api-key auth under OIDC fix)
rajivml Jun 10, 2026
8c5f4f8
web: app-wide dark mode (default) + chat UX overhaul + token theming
rajivml Jun 10, 2026
bc0ff68
web(dark): tone down bright native controls in dark mode
rajivml Jun 11, 2026
5505dd0
k8s(prod): bump web vha-79 (app-wide dark mode + chat UX)
rajivml Jun 11, 2026
e3195a0
docs: web deploy runbook (az acr build path, RBAC/SP, local SIGSEGV)
rajivml Jun 11, 2026
97d8649
web: add light/dark toggle to the global user menu
rajivml Jun 11, 2026
008f58d
fix(web): move UserDropdown theme hooks above the early return
rajivml Jun 11, 2026
b9dba5e
k8s(prod): bump web vha-80 (global light/dark toggle in user menu)
rajivml Jun 11, 2026
38588db
feat: make rerank + relevance cluster-config-driven; disable + drop GPU
rajivml Jun 11, 2026
8ef235d
k8s(prod): bump backend vha-150 (/settings exposes rerank/relevance f…
rajivml Jun 11, 2026
39c0548
k8s(prod): bump web vha-81 (hide rerank/relevance toggles when disabled)
rajivml Jun 11, 2026
8e930f6
k8s: self-managed Velero overlay for weekly Vespa backups
rajivml Jun 11, 2026
6799e32
chat: disable LLM relevance filter on the default assistants
rajivml Jun 12, 2026
747fe51
docs: Azure-managed AKS Backup runbook (reference / alternative path)
rajivml Jun 12, 2026
c42e59a
docs: local Redis via compose + shared danswer-stack network
rajivml Jun 12, 2026
a4cb409
k8s(prod): bump backend vha-151 (relevance-filter-off personas + sett…
rajivml Jun 13, 2026
74b7a3a
fix(deletion): re-drive connector deletions orphaned by lost broker m…
rajivml Jun 15, 2026
135815f
k8s(prod): bump backend vha-152 (orphaned-deletion re-drive)
rajivml Jun 15, 2026
1866555
feat(web connector): opt-in latest-N version tracking for docs.uipath
rajivml Jun 15, 2026
48bc15e
k8s(prod): bump backend vha-153 (uipath latest-N web connector)
rajivml Jun 15, 2026
ac141a7
k8s(prod): bump web vha-82 (uipath latest-N connector form field)
rajivml Jun 15, 2026
1be7f28
docs(AGENTS): slow indexing = per-doc Vespa visit; content-hash dedup…
rajivml Jun 15, 2026
0ec402e
fix(web connector): never fall back to a product-base crawl on versio…
rajivml Jun 15, 2026
286f18c
k8s(prod): bump backend vha-154 (uipath version-expand fallback harde…
rajivml Jun 15, 2026
f89fd71
fix(web connector): scope POLL sitemap to recursive_prefixes (latest-N)
rajivml Jun 15, 2026
902a448
k8s(prod): bump backend vha-155 (poll sitemap latest-N scoping)
rajivml Jun 15, 2026
0056a1e
feat(web connector): default uipath latest-N tracking to 2 versions
rajivml Jun 15, 2026
3613d83
k8s(prod): bump backend vha-156 (default latest-N=2); web Dockerfile …
rajivml Jun 15, 2026
50256e8
k8s(prod): bump web vha-83 (form default latest-N=2, ACR node base)
rajivml Jun 15, 2026
e6947b7
feat(indexing): per-source concurrency cap override (uncap web)
rajivml Jun 15, 2026
4d27607
fix(web): doc-set picker crash, EE 404 spam, indexing activity/failed…
rajivml Jun 15, 2026
c4294c1
k8s(prod): bump backend vha-157, web vha-85
rajivml Jun 15, 2026
6361ea8
fix(web): smarter connector search in document-set picker
rajivml Jun 16, 2026
5c0a0bf
k8s(prod): bump web vha-86 (document-set connector search fix)
rajivml Jun 16, 2026
1be4192
fix(web): rank connector search results best-match-first + cap rendering
rajivml Jun 16, 2026
eb01b79
k8s(prod): bump web vha-87 (best-match-first connector search ranking)
rajivml Jun 16, 2026
45ab53b
fix(db): pool_pre_ping + recycle on async engine
rajivml Jun 16, 2026
af73172
fix(api): user-friendly errors — stop leaking raw SQL/exceptions to t…
rajivml Jun 16, 2026
29b552b
feat(web): chat landing redesign + resilient SSR + dark-mode markdown…
rajivml Jun 16, 2026
2f4934e
k8s(prod): bump backend vha-158, web vha-88
rajivml Jun 16, 2026
156de60
perf(vespa): batch chunk-id visits + fix duplicate-append in bulk upd…
rajivml Jun 16, 2026
41531cb
k8s(prod): bump backend vha-159 (vespa bulk-update perf)
rajivml Jun 16, 2026
ee71b76
fix(celery): run prune check every 15m, not every 5s
rajivml Jun 16, 2026
d6dcb26
k8s(prod): bump backend vha-160 (prune-check cadence fix)
rajivml Jun 16, 2026
5026fc6
fix(celery): cap concurrent document-set syncs at 2
rajivml Jun 16, 2026
9e887f5
k8s(prod): bump backend vha-161 (cap doc-set sync concurrency at 2)
rajivml Jun 16, 2026
a32af28
fix(vespa): skip documents with no chunks in bulk update (KeyError re…
rajivml Jun 16, 2026
c2e374c
k8s(prod): bump backend vha-162 (vespa bulk-update KeyError fix)
rajivml Jun 16, 2026
6bbff90
feat(celery): resumable document-set sync via persisted cursor
rajivml Jun 17, 2026
8bafbd6
k8s(prod): bump backend vha-163 (resumable doc-set sync)
rajivml Jun 17, 2026
0e37b0c
perf(celery): raise document-set sync concurrency cap 2 -> 3
rajivml Jun 17, 2026
d302921
k8s(prod): bump backend vha-164 (doc-set sync cap 3)
rajivml Jun 17, 2026
e78b675
feat(backend): chat history pagination, persona display_name, docs-ve…
rajivml Jun 19, 2026
c5a8492
feat(web): chat readability + assistants UX + lazy-loaded sidebar his…
rajivml Jun 19, 2026
873b2be
ops(prod): Apple-Silicon web cloud-build routing, dedup/Highspot env,…
rajivml Jun 19, 2026
765aa47
feat(outsystems): inside.uipath.com connector with large-doc hardening
rajivml Jun 21, 2026
f3fbacd
fix(web/admin): responsive indexing tabs + sidebar active state
rajivml Jun 21, 2026
224947d
fix(ops): verify image in registry before apply; bump prod tags
rajivml Jun 21, 2026
2688484
k8s(prod): bump web=vha-102 (admin UX fixes: sidebar active-tab, stat…
rajivml Jun 21, 2026
86cfb78
feat(assistants): opt-out visibility — new assistants appear for all …
rajivml Jun 21, 2026
cb44197
k8s(prod): bump backend=vha-184 web=vha-103 (opt-out assistant visibi…
rajivml Jun 21, 2026
9269881
ops(prod): add outsystems to PROTECTED_SOURCES (source-diversity rese…
rajivml Jun 21, 2026
0811494
feat(search): source-reserved retrieval — guarantee curated sources r…
rajivml Jun 21, 2026
e61bf14
k8s(prod): bump backend=vha-185 (source-reserved retrieval)
rajivml Jun 21, 2026
df81008
fix(search): build supplemental query via copy(update=) — SearchQuery…
rajivml Jun 21, 2026
01d2008
k8s(prod): bump backend=vha-186 (source-reserved retrieval immutabili…
rajivml Jun 21, 2026
b5ff3e5
feat(answering): cap docs-per-source in prompt so curated sources get…
rajivml Jun 21, 2026
9c4f3a6
k8s(prod): bump backend=vha-187 (per-source doc cap)
rajivml Jun 21, 2026
d952d34
feat(prompts): global authoritative-sources citation nudge (from PROT…
rajivml Jun 21, 2026
88ac48c
k8s(prod): bump backend=vha-188 (authoritative-sources citation nudge)
rajivml Jun 21, 2026
10c1ee3
feat(prompts): strengthen authoritative-sources nudge to mandatory ci…
rajivml Jun 21, 2026
beae838
k8s(prod): bump backend=vha-189 (mandatory authoritative-sources nudge)
rajivml Jun 21, 2026
a5a08e0
feat(prompts): grouped citations — 'Authoritative sources' vs 'Other …
rajivml Jun 21, 2026
6c50272
k8s(prod): bump backend=vha-190 (grouped authoritative citations)
rajivml Jun 21, 2026
66b172b
fix(prompts): revert to soft authoritative-source suggestion (drop gr…
rajivml Jun 21, 2026
36eb4e5
k8s(prod): bump backend=vha-191 (soft authoritative suggestion)
rajivml Jun 21, 2026
114a2ad
feat(answering): verify-then-retain authoritative citations (post-gen…
rajivml Jun 22, 2026
eb2b834
k8s(prod): bump backend=vha-192 (verify-then-retain authoritative cit…
rajivml Jun 22, 2026
19bc09c
feat(answering): retry the authoritative-citation verify call once
rajivml Jun 22, 2026
24bcb6e
k8s(prod): bump backend=vha-193 (verify-call retry hardening)
rajivml Jun 22, 2026
4517f61
feat(answering): merge retained authoritative citation into Sources +…
rajivml Jun 22, 2026
b4616f1
k8s(prod): bump backend=vha-194 (authoritative citation merged into S…
rajivml Jun 22, 2026
ac5da59
revert(answering): back to authoritative footer (merge-into-Sources c…
rajivml Jun 22, 2026
49a7674
k8s(prod): bump backend=vha-195 (revert to authoritative footer)
rajivml Jun 22, 2026
df395f0
feat(answering): loosen authoritative-source verify to relevance
rajivml Jun 22, 2026
59adfa6
k8s(prod): bump backend=vha-196 (loosen authoritative verify to relev…
rajivml Jun 22, 2026
0d6e6a7
fix(answering): surface uncited authoritative docs even if another wa…
rajivml Jun 22, 2026
228ec01
k8s(prod): bump backend=vha-197 (surface uncited authoritative docs)
rajivml Jun 22, 2026
842feaa
fix(answering): tighten authoritative-relevance verify (same-product …
rajivml Jun 22, 2026
db1e645
k8s(prod): bump backend=vha-198 (tighten authoritative-relevance verify)
rajivml Jun 22, 2026
83e58b0
fix(answering): generalize relevance verify (drop AI-specific example)
rajivml Jun 22, 2026
1c18541
k8s(prod): bump backend=vha-199 (generalize relevance verify)
rajivml Jun 22, 2026
0c3c0aa
feat(answering): verify relevance against the matched chunk (reliable…
rajivml Jun 22, 2026
87cfa0c
k8s(prod): bump backend=vha-200 (verify relevance against matched chunk)
rajivml Jun 22, 2026
0ba92a3
ops(prod): bump SOURCE_RESERVED_RETRIEVAL_SLOTS 3->6
rajivml Jun 22, 2026
b37ba2c
feat(answering): query-time rewrite of versioned docs links to latest…
rajivml Jun 22, 2026
b44ecbd
k8s(prod): bump backend=vha-201 (rewrite docs links to latest indexed…
rajivml Jun 22, 2026
a4ff815
feat(answering): version-aware docs link rewrite
rajivml Jun 22, 2026
3379bc0
k8s(prod): bump backend=vha-202 (version-aware docs link rewrite)
rajivml Jun 22, 2026
f4c7131
fix(answering): anchor authoritative relevance verify to the question
rajivml Jun 22, 2026
8b02462
fix(answering): simplify verify prompt to relevance-vs-(question AND …
rajivml Jun 22, 2026
6cd63fd
k8s(prod): bump backend=vha-204 (simplified verify prompt)
rajivml Jun 22, 2026
8b4f606
docs(search-quality): document the source-prioritization + authoritat…
rajivml Jun 22, 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
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ requestdata.json
# Playwright MCP session artifacts (console logs, page snapshots, ad-hoc
# screenshots) written during local UI debugging. Not source.
.playwright-mcp/
model-picker-open.png

# Live cluster dumps from `kubectl get -o yaml > …`. NEVER commit:
# Darwin's ConfigMap currently contains real secrets in plaintext (Slack
Expand All @@ -39,3 +38,9 @@ model-picker-open.png
darwin-kubernetes/temp/
k8s/overlays/*/secrets.env
k8s/overlays/*/*.secrets.env
# Velero Azure SP credentials (source for the cloud-credentials secret)
k8s/overlays/prod-velero/credentials-velero
# Velero notifier Slack bot token (source for the slack-notify secret)
k8s/overlays/prod-velero/slack-notify.env
# Ad-hoc export of web connector URLs (local only)
web-connectors.csv
72 changes: 72 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,78 @@ liveness probes by design** (an aggressive one kills slow-but-healthy
nodes); readiness probes on the Service-backed nodes
(configserver/query/feed) gate traffic during the slow bootstrap.

### 11. Slow indexing is the per-doc Vespa existence VISIT, not the crawl — and the content-hash dedup already exists

When a connector (especially a big web one like docs.uipath) indexes slowly,
the bottleneck is almost never the source fetch. For every document,
`VespaIndex.index()` → `_clear_and_index_vespa_chunks()` →
`_get_vespa_chunks_by_document_id` (`document_index/vespa/index.py`) hits Vespa's
**Visit API** (`GET /document/v1/.../docid?selection=document_id=='<id>'&wantedDocumentCount=1000`)
to find existing chunks before re-writing. That `selection` is a **corpus scan**,
~**10–11 s per document** on the large prod index — and Vespa content nodes sit
near-idle while it happens (it's scan/IO-bound, so scaling content nodes does
NOT help). It's in the shared indexing path, so it slows every connector; large
multi-doc connectors just make it obvious. The real fix is a keyed lookup
(`document_id` as a `fast-search` attribute, or a point GET / search query)
instead of the visit.

**Do NOT "add" a Postgres content-hash dedup to skip this — it already exists.**
`Document.indexed_content_hash` (`db/models.py`) +
`get_doc_ids_to_update` (`indexing/indexing_pipeline.py`) skip a doc (no re-embed,
no Vespa write) when the stored hash equals `doc.get_content_hash()`. The hash is
written only AFTER a confirmed Vespa write. Why it can still re-index everything:

- It's bypassed when `ignore_time_skip=True`, set on `from_beginning` full runs
(`background/indexing/run_indexing.py`).
- Docs indexed before the hash feature have `indexed_content_hash = NULL`, so the
hash check can't fire and it falls back to a `doc_updated_at` timestamp compare.
- The **web connector never sets `doc_updated_at`**, so that fallback can't skip
hash-less web docs either → they re-index every run (each paying the ~11 s
visit) UNTIL the run completes and backfills their hash. It is self-healing —
once hashes exist, later polls skip unchanged docs and run fast — but a full
run that times out before backfilling will keep re-doing the slow work.

(Diagnosed 2026-06 on the docs.uipath automation-suite latest-N connector:
~2889 of ~3161 docs had NULL hashes.)

---

### 12. NEVER build the web image locally on Apple Silicon

The web image's `next build` step **SIGSEGVs** when built for `linux/amd64`
under emulation on an arm64 Mac (Next.js build worker dies with `signal:
SIGSEGV`). Building amd64 under emulation is the only way to produce a
deployable image locally on Apple Silicon, so there is no working local web
build there — don't try, and don't burn time "fixing" it. It is not a config /
dependency / disk-space problem.

Instead, build web on **darwinacr** (native-amd64 ACR build agents) and import
the result into the prod registry. `k8s/scripts/build-deploy.sh` does this
**automatically** on Apple Silicon — `build-deploy.sh deploy web` detects the
host and routes web to `az acr build` + `az acr import`, no flags needed. The
backend image has no native build step and still builds locally under emulation.

If you ever need the raw commands (script unavailable / debugging):

```bash
# 1. build on darwinacr (native amd64)
az acr build --registry darwinacr \
--image danswer/danswer-web-server:vha-N \
--build-arg NODE_BASE=darwinacr.azurecr.io/library/node:20-alpine \
--file web/Dockerfile ./web
# 2. transfer darwinacr -> prod registry (different subscriptions, so blob-copy
# via pull/retag/push, NOT `az acr import`). Pure copy on the Mac, no SIGSEGV.
az acr login --name darwinacr
docker pull --platform linux/amd64 darwinacr.azurecr.io/danswer/danswer-web-server:vha-N
docker tag darwinacr.azurecr.io/danswer/danswer-web-server:vha-N \
sfbrdevhelmweacr.azurecr.io/danswer/danswer-web-server:vha-N
docker push sfbrdevhelmweacr.azurecr.io/danswer/danswer-web-server:vha-N
```

(`--file` is relative to the CWD, not the `./web` context — `web/Dockerfile`,
not `Dockerfile`. `az acr build` on darwinacr needs **PIM Contributor**; the
prod push uses the `~/.zshrc` ACR_USERNAME/ACR_PASSWORD admin creds.)

---

## Common workflows
Expand Down
46 changes: 35 additions & 11 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,21 +112,21 @@ playwright install
#### Dependent Docker Containers
First navigate to `danswer/deployment/docker_compose`, then start Postgres.

The simplest path is the compose-managed pair (uses a named docker volume for
Vespa's data; data lives until you `docker volume rm`):
Start Postgres and Redis via compose under the `-p danswer-stack` project, so
they share the `danswer-stack_default` network (Vespa is run separately, below,
on the same network):

```bash
docker compose -f docker-compose.dev.yml -p danswer-stack up -d relational_db
docker compose -f docker-compose.dev.yml -p danswer-stack up -d relational_db redis
```

If you'd rather pin Vespa's data + logs to host-mounted directories so you
can inspect them outside Docker (and survive `docker compose down -v`),
start Postgres via compose and Vespa via a manual `docker run` on the same
network. Pick any host paths you like:
Run Vespa via a manual `docker run` on that same `danswer-stack_default`
network, with host-mounted data + logs dirs so you can inspect them outside
Docker (and they survive `docker compose down -v`). Use this rather than the
compose `index` service (it's unreliable locally); the `--network` flag is what
keeps the manually-run Vespa on the shared network. Pick any host paths:

```bash
docker compose -f docker-compose.dev.yml -p danswer-stack up -d relational_db

export VESPA_VAR_STORAGE="${HOME}/danswer-vespa-data/var"
export VESPA_LOG_STORAGE="${HOME}/danswer-vespa-data/logs"
mkdir -p "$VESPA_VAR_STORAGE" "$VESPA_LOG_STORAGE"
Expand All @@ -142,14 +142,27 @@ docker run \
--publish 19071:19071 \
vespaengine/vespa:8.277.17

# Sanity check: both containers should be on the danswer-stack_default network
# Sanity check: all containers (Postgres, Redis, Vespa) on danswer-stack_default
docker ps --format '{{ .ID }} {{ .Names }} {{ json .Networks }}'
```

(index refers to Vespa and relational_db refers to Postgres. The hostname
`index` matters — Danswer reaches Vespa by that DNS name on the shared
network.)

Redis (caching + per-user rate limiting) comes up with the commands above as
part of the `danswer-stack` project, so it's already on the shared
`danswer-stack_default` network. To check it or manage it on its own:

```bash
docker compose -f docker-compose.dev.yml -p danswer-stack exec redis redis-cli ping # -> PONG
docker compose -f docker-compose.dev.yml -p danswer-stack stop redis
```

The container runs with no auth and publishes `6379` to the host, so a
host-run backend connects with `REDIS_HOST=localhost`, `REDIS_PORT=6379`,
`REDIS_PASSWORD=` (empty). (In-compose, the service name is `redis`.)

#### Running Danswer
To start the frontend, navigate to `danswer/web` and run:
```bash
Expand Down Expand Up @@ -325,7 +338,18 @@ export MODEL_SERVER_HOST=localhost
export MODEL_SERVER_PORT=9000
export INDEXING_MODEL_SERVER_HOST=localhost
export INDEXING_MODEL_SERVER_PORT=9000
export REDIS_HOST=cache # matches the compose service name
export REDIS_HOST=localhost # backend runs on the host; reach Redis via the published 6379 port

# Cross-encoder reranking, available locally. The model server (`dmo`) loads the
# reranker IN-PROCESS (sentence-transformers, CPU) — no extra container. Uses the
# small default model (mxbai-rerank-xsmall-v1); set RERANK_MODEL_NAME to try a
# bigger one. Reranking still only runs for assistants / chats that opt in.
# (Prod serves the reranker via a TEI container instead — see k8s/optional/tei-rerank.)
export RERANK_ENABLED=true
export LLM_RELEVANCE_FILTER_ENABLED=true # LLM relevance filter; independent of rerank
# Advanced: to mirror prod and offload the reranker to a local TEI container
# instead of in-process, run TEI yourself and set:
# export RERANK_SERVER_URL=http://localhost:8086

# ---------------------------------------------------------------------------
# LLM (Generative AI) — UiPath LLM Gateway via OAuth client credentials
Expand Down
35 changes: 35 additions & 0 deletions backend/alembic/versions/a8b9c0d1e2f3_persona_display_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""persona: add display_name (user-friendly chat label)

Adds persona.display_name — an optional, admin-editable label shown in the chat
UI. The immutable `name` stays the identifier; `display_name` is presentational
only and the chat falls back to `name` when it's blank. Backfills existing rows
with their `name` so nothing changes visually until an admin edits it. See
db/models.py::Persona.

Revision ID: a8b9c0d1e2f3
Revises: f7a8b9c0d1e2
Create Date: 2026-06-19

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "a8b9c0d1e2f3"
down_revision = "f7a8b9c0d1e2"
branch_labels: None = None
depends_on: None = None


def upgrade() -> None:
op.add_column(
"persona",
sa.Column("display_name", sa.String(), nullable=True),
)
# Backfill: existing assistants keep showing their current name.
op.execute("UPDATE persona SET display_name = name WHERE display_name IS NULL")


def downgrade() -> None:
op.drop_column("persona", "display_name")
46 changes: 46 additions & 0 deletions backend/alembic/versions/b9c0d1e2f3a4_user_hidden_assistants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""user: add hidden_assistants (opt-out assistant visibility)

Adds user.hidden_assistants — the list of assistant (persona) ids a user has
explicitly hidden from their chat picker. This flips assistant visibility from
opt-IN (only assistants in `chosen_assistants` were shown) to opt-OUT: every
accessible assistant is visible by default, so a newly created admin assistant
appears for all users automatically; a user hides the ones they don't want.

`chosen_assistants` now controls ORDER/default only, not visibility.

No backfill: the chat experience hasn't been rolled out to end users yet, so
there is no curated state to preserve — every existing user simply starts with
an empty hidden list (= sees everything), which is the desired behavior. See
db/models.py::User.

Revision ID: b9c0d1e2f3a4
Revises: a8b9c0d1e2f3
Create Date: 2026-06-21

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql


# revision identifiers, used by Alembic.
revision = "b9c0d1e2f3a4"
down_revision = "a8b9c0d1e2f3"
branch_labels: None = None
depends_on: None = None


def upgrade() -> None:
op.add_column(
"user",
sa.Column(
"hidden_assistants",
postgresql.ARRAY(sa.Integer()),
nullable=False,
server_default="{}",
),
)


def downgrade() -> None:
op.drop_column("user", "hidden_assistants")
39 changes: 39 additions & 0 deletions backend/alembic/versions/f6a7b8c9d0e1_persona_rerank_enabled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""persona: add rerank_enabled (per-assistant cross-encoder reranking opt-in)

Per-assistant toggle for cross-encoder reranking. Only takes effect when
reranking is globally available (RERANK_ENABLED + a GPU-backed model server);
default false so existing assistants and the GPU-free local/default setup are
unchanged. Lets reranking be rolled out incrementally / A-B compared per
assistant before becoming the default. See db/models.py::Persona and
search/preprocessing/preprocessing.py.

Revision ID: f6a7b8c9d0e1
Revises: e5f6a7b8c9d0
Create Date: 2026-06-03

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "f6a7b8c9d0e1"
down_revision = "e5f6a7b8c9d0"
branch_labels: None = None
depends_on: None = None


def upgrade() -> None:
op.add_column(
"persona",
sa.Column(
"rerank_enabled",
sa.Boolean(),
nullable=False,
server_default=sa.false(),
),
)


def downgrade() -> None:
op.drop_column("persona", "rerank_enabled")
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""slack bot: response blocklist (suppress responses for certain senders)

Creates slack_bot_response_blocklist — senders (by email) whose Slack messages
should NOT trigger a Darwin response. DB-driven so the list can change without a
redeploy. Seeds the first entry (jr.bancel@uipath.com). See
db/models.py::SlackBotResponseBlocklist and
danswerbot/slack/handlers/handle_message.py.

Revision ID: f7a8b9c0d1e2
Revises: f6a7b8c9d0e1
Create Date: 2026-06-17

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "f7a8b9c0d1e2"
down_revision = "f6a7b8c9d0e1"
branch_labels: None = None
depends_on: None = None


def upgrade() -> None:
op.create_table(
"slack_bot_response_blocklist",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("email", sa.String(), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
sa.PrimaryKeyConstraint("id"),
)
# Single unique index — mirrors `mapped_column(String, unique=True, index=True)`.
op.create_index(
op.f("ix_slack_bot_response_blocklist_email"),
"slack_bot_response_blocklist",
["email"],
unique=True,
)

# Seed the initial blocked senders (stored lowercase; matched
# case-insensitively). Further additions are plain DB inserts — no migration.
op.execute(
sa.text(
"INSERT INTO slack_bot_response_blocklist (email) VALUES "
"('jr.bancel@uipath.com'), ('andrei.barbu@uipath.com') "
"ON CONFLICT (email) DO NOTHING"
)
)


def downgrade() -> None:
op.drop_index(
op.f("ix_slack_bot_response_blocklist_email"),
table_name="slack_bot_response_blocklist",
)
op.drop_table("slack_bot_response_blocklist")
32 changes: 32 additions & 0 deletions backend/danswer/auth/api_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,35 @@ def validate_api_key(request: Request, db_session: Session = Depends(get_session
# Cache it for future requests
cache[api_key_value] = True
return None


def request_has_valid_api_key(request: Request, db_session: Session) -> bool:
"""Return True if the request carries a valid X-API-Key.

These keys are service credentials for automation (they intentionally do NOT
map to a browser `User`). `current_user` uses this to authorize an api-key
request as an anonymous service caller instead of 403'ing it into the SSO
flow once AUTH_TYPE enforces auth (e.g. OIDC). Mirrors `validate_api_key`'s
lookup + cache exactly, so the two stay consistent.

NOTE: `db_session` is passed in (not a Depends) because the caller already
holds a session.
"""
if _API_KEY_HEADER not in request.headers:
return False

api_key_value = request.headers.get(_API_KEY_HEADER)
if not api_key_value:
return False

if api_key_value in cache:
return True

api_key = db_session.scalar(
select(ApiKey).where(ApiKey.hashed_api_key == api_key_value)
)
if api_key is None:
return False

cache[api_key_value] = True
return True
Loading
Loading