Skip to content

Docs & Guide Assistants

Two "ask a question, get a synthesized answer with source citations" widgets, shipped in v4.55 — Ask the guide on the public help.objectuve.com and Ask the docs on the Clerk-gated internal docs.objectuve.com. Both live inside VitePress's existing local-search modal rather than as a separate launcher — see the UI-SPEC for the full design rationale and every state's copy.

For runtime controls (kill switch, budget ceiling, rate limits, corpus refresh), see Docs & guide assistant operations. For the retrieval architecture decision and measured accuracy numbers, see the spike ADR.

The two surfaces, and why they're built differently

Ask the guideAsk the docs
Sitehelp.objectuve.com (guide_site/)docs.objectuve.com (docs_site/)
Corpus22 public pages, ~16k tokens — committed whole464 internal files, 10,667 heading chunks — strategy/revenue/roadmap material
RetrievalFull-context stuffing (corpus fits in one prompt)BM25 lexical retrieval at k=5, Postgres-backed
GraphQL operationaskGuideQuestionaskDocsQuestion
AuthUnauthenticatedClerk-gated + @objectuve.com-restricted

The corpus-size gap is why the two use different retrieval strategies, not a stylistic choice — see the ADR for the full reasoning and the measured recall/citation numbers each strategy produces.

GraphQL surface

Both operations are documented in the generated GraphQL API ReferenceaskGuideQuestion and askDocsQuestion under Queries, GuideAssistantAnswer / DocsAssistantAnswer under Types. What the generated reference can't say is the one thing that matters most about these two operations:

The auth asymmetry — the part the generator can't say

askGuideQuestion is unauthenticated by design. The guide corpus is public-facing content, so there's nothing to gate. Anonymous callers are identified by IP for rate-limiting purposes only ("anon:<ip>", rails_api/app/graphql/types/query_type.rb:807).

askDocsQuestion is never anonymous, and its check is server-side, not client-side. The docs corpus contains strategy, revenue, and roadmap material. docs_site/.vitepress/theme/AuthGate.vue already gates the site to @objectuve.com (ALLOWED_DOMAIN, AuthGate.vue:8) — but that's a client-side UX affordance, not a security boundary a GraphQL field can rely on. So Types::QueryType#ask_docs_question (rails_api/app/graphql/types/query_type.rb:839-846) independently verifies, in this order, before any settings check or LLM call:

  1. A real Clerk session exists (require_auth! — the same UserIdentity::AuthenticateSessionToken path GraphqlController#current_user uses everywhere else in the API).
  2. current_user.email ends with @objectuve.com — checked against the literal domain string (DOCS_ASSISTANT_ALLOWED_EMAIL_DOMAIN, query_type.rb:9), not against AuthGate.vue's value.

A request with no session, an invalid session, or a valid session on a non-@objectuve.com email all fail this check before touching the kill switch, the rate limiter, or LiteLLM. The third case is the one that matters most — a real product user with a working Clerk session must get nothing from this field, and that's enforced here, not by the site shell around it.

Response shape

Both operations return a state discriminator (ANSWERED / REFUSED / DISABLED) plus answer and citations. RATE_LIMITED and any other failure arrive as a coded GraphQL error (extensions.code), not as a state value — a refusal and a disabled assistant are both successful responses to an authorized caller; a rate limit or an outage is not.

  • GuideAssistantCitationslug, title, path (already a resolvable help.objectuve.com URL).
  • DocsAssistantCitationpath (a repo-relative source path like docs/architecture/ddd/01-bounded-contexts.md), title. The client maps this to a docs.objectuve.com route itself (docs_site/.vitepress/theme/askDocsQuestion.ts's sourceHref — strips the docs/ prefix and .md suffix, folds a trailing index to /).

Every citation is validated against the real corpus before it's returned — a cited slug/path that doesn't resolve to an actual corpus page is dropped, never surfaced. This is the prompt-injection floor: corpus content is, in principle, attacker-influenced (docs/ is edited by agents), so a citation can never point a caller outside the corpus regardless of what the model was tricked into citing.

Refusal behavior

Both assistants answer only from their own corpus. When a question can't be answered from it, the model is instructed to return an exact phrase rather than guess — the Interaction detects that phrase and returns state: REFUSED with no citations. Each assistant has its own distinct constant and phrase, not a shared one: Ai::DocsAssistant::AnswerGuideQuestion::REFUSAL_PHRASE = "Not covered in this guide." for the guide, and Ai::DocsAssistant::AnswerDocsQuestion::REFUSAL_PHRASE = "Not covered in the docs." for the docs assistant (answer_docs_question.rb:20) — both matched case-insensitively.

Only the guide's phrase has been verified against a live model. Phase 4 round-tripped a live out-of-scope question through askGuideQuestion and confirmed the model returned the literal REFUSAL_PHRASE — closing the risk that a live model might not reproduce it verbatim and silently turn a refusal into a confabulated answer. The docs assistant's own refusal path (answer_docs_question_spec.rb) is tested only against a stubbed Ai::ServiceClient.chat returning the phrase directly — there is no recorded live-model round-trip confirming gemini-3.8-flash reproduces "Not covered in the docs." verbatim the way it does the guide's phrase.

Retrieval quality

Both surfaces are guarded by a CI-enforced regression check tracking recall and citation accuracy as separate metrics — a retrieval miss and a citation miss are different failures with different fixes. Recall@5 on the docs corpus is 100% (22/22), beating the spike's own 90.9% floor after a title-boost fix. Citation accuracy is measured against fixtures in CI; the spike's fixture-derived baseline (76.9% @ k=5) has not yet been re-measured against a live model — tracked as OBJ-3654. See the ADR for the full numbers and the BM25 count-question gotcha (now fixed) in Gotchas.

Availability

Both operations ship behind Settings.ai.docs_assistant_enabled (see Docs & guide assistant operations for the kill switch, budget ceiling, and rate limits) and reach real users on the next production release train — see CHANGELOG.md for the release this shipped in.

Last updated: 2026-09-12 (OBJ-3624, PR #3101 review: corrected the Refusal behavior section — the docs assistant has its own distinct REFUSAL_PHRASE constant/text, not the guide's, and only the guide's phrase has a recorded live-model verification. Prior update 2026-09-11: initial page)

Loading…