Building an AI Assistant: Product Help and Navigation

Today

A user is looking at your application and asks, "How do I add a donation, and where do I go?"

A useful AI assistant needs to answer two different questions. It must explain the product's workflow using reliable documentation, then offer a route the user can actually access. Searching documentation does not grant permission to open a screen, and knowing a route does not explain how a feature works.

This first tutorial builds those two paths. By the end, you should be able to design a knowledge pipeline, separate search from reading, and turn an AI agent's navigation suggestion into a checked, user-clicked button.

Our running example is a fundraising management product used by charity staff. A donation is a received gift; a donor is the person or organization giving it. A pledge is a promise to give later. You do not need fundraising experience to follow the series: these distinctions serve the same role as orders versus payments in commerce software.

What Makes This an Agentic System?#

Here, an AI assistant is the user-facing experience. The agentic part is the bounded loop in which a model selects tools, inspects their results, and decides what to do next. It might search a help article, read it, find a screen, and propose a button before answering.

That autonomy is deliberately narrow. The model interprets the request; the application defines the tools and decides whether their arguments are valid and authorized. This is enough to build useful agentic behavior without giving a model arbitrary database or browser access.

Shared Zod contracts describe tool inputs and streamed UI artifacts. A reusable chat panel renders them, while orchestration stays inside the product backend, next to existing authentication and tenant access. Start there if your product already has those boundaries; extracting another service is a separate operational decision.

Build a Knowledge Pipeline Before a Search Tool#

Documentation needs an owner and a release path. In this example, product editors author help in Notion. An exporter converts the pages into Markdown, and a manually triggered workflow opens or updates a review pull request. Reviewed files become part of the deployed backend.

Notion pages
    -> export Markdown
    -> review content changes in a pull request
    -> deploy articles with the application
    -> load and index the corpus at backend startup

The assistant searches deployed Markdown, not live Notion. This makes the knowledge version reproducible and keeps runtime answers independent of Notion availability. The tradeoff is freshness: an edit is not answerable until export, review, and deployment complete.

An exporter needs to preserve more than plain paragraphs. Headings, rich text, tables, and links carry meaning. Stable filenames matter because they become article IDs. If an export renames a page unexpectedly, references to its former ID can break.

For a generalized article, frontmatter might look like this. This is an illustrative authoring format, not a copy of the exporter's exact schema:

---
title: "Adding a Donation"
source: "https://help.example.com/adding-a-donation"
published: true
---
 
## Before You Start
 
Check that you have permission to view and add donations.
 
## Add the Gift
 
Open Donations, choose Add, and enter the received gift details.

Load only answerable content. Exclude drafts, authoring instructions, tables of contents, and revision histories. Otherwise a page containing every feature name can outrank the actual instructions while telling the user nothing useful. Where a public source exists, rewrite internal article links to it so citations remain usable outside the authoring workspace.

Use Retrieval to Find Evidence, Not to Produce an Answer#

Retrieval-augmented generation, or RAG, means supplying relevant source material before the model writes its answer. That does not require a vector database in every implementation.

For a small authored corpus, weighted keyword search with BM25 is a practical starting point. BM25 ranks documents using matching terms, their frequency, and document length. The reference implementation adds title bonuses, light stemming, stop words, and a small synonym set. Titles weigh more than headings, which weigh more than body text.

Suppose the user asks, "Where do I enter a gift?" while the article says "Adding a Donation." A synonym connecting gift and donation can help retrieval. Repeating "donation" twenty times in an article is a much worse editorial fix than giving it a clear title and useful vocabulary.

The limit is lexical mismatch: if the question shares no useful terms with the corpus, keyword retrieval can miss the right article. Inspect failed searches first. Improve the content or synonym coverage, then evaluate whether semantic retrieval would address the remaining failures.

Split the model's work into two tools:

search_support("enter a gift")
    -> short candidates: article ID, title, snippet
 
read_support_article("adding-a-donation")
    -> bounded article text and source link
 
answer
    -> instructions supported by the article actually read

Search returns up to five candidates. Reading is bounded to 12,000 characters in the reference implementation. This keeps irrelevant pages out of the prompt and makes the source selection inspectable. A snippet helps choose a source; it is not enough evidence for a detailed procedural answer.

The support reader checks that an article ID exists. It does not enforce that the ID came from a preceding search in the same turn. The prompt encourages search followed by reading; navigation, below, adds a stronger provenance check in code. Keep those guarantees distinct.

Model Navigation as a Proposal#

A model-generated URL creates too much ambiguity. It can refer to an obsolete route, bypass a UI guard, or include unexpected parameters. Use a registry of stable screen IDs instead.

An illustrative registry entry is:

const screen = {
  id: "donations.list",
  title: "Donations",
  description: "View recorded gifts and begin donation entry",
  requiredPermissions: ["view:donations"],
  path: "/donations",
};

The registry belongs to the application. Search filters entries by the authenticated user's permissions and enabled product modules. The tool response can expose the ID and description without asking the model to construct a path.

Keep a set of screen IDs found during the current turn. A proposal must use one of those IDs and pass an access check again. The following is illustrative pseudocode for the enforcement pattern:

const found = new Set<string>();
 
function searchScreens(query: string, context: TurnContext) {
  const screens = appMap.search(query, context);
  for (const screen of screens) found.add(screen.id);
  return screens;
}
 
function proposeScreen(id: string, context: TurnContext) {
  if (!found.has(id)) return { error: "Search for the screen first" };
  const screen = appMap.find(id, context);
  if (!screen) return { error: "Screen unavailable" };
  return { id: screen.id, title: screen.title };
}

The tool proposes a button. The user clicks it. The frontend checks its router mapping and current access before opening the screen. These checks catch fabricated IDs, stale mappings, and access changes; a successful tool call does not make the browser's context permanently valid.

Follow One Question Through the System#

For "How do I add a donation, and where do I go?", the agent searches the deployed corpus and reads the relevant instructions. It then searches eligible screens and proposes the discovered donation screen. The answer explains the workflow, links its source, and presents a button.

If no relevant article exists, it should acknowledge the documentation gap. If the user cannot access the screen, it should not offer a button. Help and navigation can fail independently, so preserve the useful part of the answer without pretending both succeeded.

The model loop also needs an end. The reference permits six tool-enabled model steps and a final answer-only step. Parallel calls can share a step, so this is not six individual tool invocations. The final step must use successful lookups or explain why it could not answer. Browser cancellation should propagate to the running work.

Build and Check This Slice First#

Implement the content pipeline, corpus loader, search/read tools, screen registry, and proposal UI before adding business data. Test excluded drafts, rewritten source links, unfamiliar wording, invalid IDs, missing permissions, and module restrictions.

Then inspect complete answers: did the model read the article it cited, explain the task accurately, and propose only an eligible screen? Deterministic tool tests and answer evaluations serve different purposes.

Once this slice is useful, Part 2 adds business data queries. The same principle carries forward: the model chooses a declared operation, and the backend owns its meaning.

About This Series#

These tutorials use a reviewed implementation snapshot (ae7c6e6fca71) as a concrete reference, with synthetic examples and generalized identifiers. They do not describe a verified live deployment. Code marked illustrative explains a pattern rather than providing a complete application. Proposed improvements are labeled explicitly.

  1. Building an AI Assistant: Product Help and Navigation
  2. AI Agents for Business Data: A Semantic Query Engine
  3. AI Assistant Credits: Token Accounting and Usage Limits
  4. AI Agent Security: Permissions, Prompt Injection, and Safe Actions