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 guide | Ask the docs | |
|---|---|---|
| Site | help.objectuve.com (guide_site/) | docs.objectuve.com (docs_site/) |
| Corpus | 22 public pages, ~16k tokens — committed whole | 464 internal files, 10,667 heading chunks — strategy/revenue/roadmap material |
| Retrieval | Full-context stuffing (corpus fits in one prompt) | BM25 lexical retrieval at k=5, Postgres-backed |
| GraphQL operation | askGuideQuestion | askDocsQuestion |
| Auth | Unauthenticated | Clerk-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 Reference — askGuideQuestion 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:
- A real Clerk session exists (
require_auth!— the sameUserIdentity::AuthenticateSessionTokenpathGraphqlController#current_useruses everywhere else in the API). current_user.emailends with@objectuve.com— checked against the literal domain string (DOCS_ASSISTANT_ALLOWED_EMAIL_DOMAIN,query_type.rb:9), not againstAuthGate.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.
GuideAssistantCitation—slug,title,path(already a resolvablehelp.objectuve.comURL).DocsAssistantCitation—path(a repo-relative source path likedocs/architecture/ddd/01-bounded-contexts.md),title. The client maps this to adocs.objectuve.comroute itself (docs_site/.vitepress/theme/askDocsQuestion.ts'ssourceHref— strips thedocs/prefix and.mdsuffix, folds a trailingindexto/).
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)