Compare commits

..

3 Commits

Author SHA1 Message Date
Steven H a0c1bdf58e Improve the Ask prompt for llms.txt (#4674) 2026-10-09 12:09:23 +01:00
Steven H d685660e1d Ensure we send the agent goal with the ask (#4673) 2026-10-08 20:51:24 +01:00
Peter White f1d5123c0b Support fr-ca, es-mx and es-419 site UI locales (RND-13260, RND-11280) (#4671) 2026-10-08 17:10:49 +02:00
16 changed files with 111 additions and 245 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Compress oversized site API tokens in the rewritten URL to stay under URL length limits.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Agents can now send a goal with any question they ask on the Markdown version of a page.
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Show site UI in French or Spanish for spaces set to Canadian French, Mexican Spanish or Latin American Spanish.
-3
View File
@@ -143,7 +143,6 @@
"direction": "^2.0.1", "direction": "^2.0.1",
"event-iterator": "^2.0.0", "event-iterator": "^2.0.0",
"feed": "^5.1.0", "feed": "^5.1.0",
"fflate": "^0.8.3",
"flexsearch": "^0.8.212", "flexsearch": "^0.8.212",
"image-size": "^2.0.2", "image-size": "^2.0.2",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
@@ -2118,8 +2117,6 @@
"feed": ["feed@5.1.0", "", { "dependencies": { "xml-js": "^1.6.11" } }, "sha512-qGNhgYygnefSkAHHrNHqC7p3R8J0/xQDS/cYUud8er/qD9EFGWyCdUDfULHTJQN1d3H3WprzVwMc9MfB4J50Wg=="], "feed": ["feed@5.1.0", "", { "dependencies": { "xml-js": "^1.6.11" } }, "sha512-qGNhgYygnefSkAHHrNHqC7p3R8J0/xQDS/cYUud8er/qD9EFGWyCdUDfULHTJQN1d3H3WprzVwMc9MfB4J50Wg=="],
"fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="],
"file-entry-cache": ["file-entry-cache@10.0.7", "", { "dependencies": { "flat-cache": "^6.1.7" } }, "sha512-txsf5fu3anp2ff3+gOJJzRImtrtm/oa9tYLN0iTuINZ++EyVR/nRrg2fKYwvG/pXDofcrvvb0scEbX3NyW/COw=="], "file-entry-cache": ["file-entry-cache@10.0.7", "", { "dependencies": { "flat-cache": "^6.1.7" } }, "sha512-txsf5fu3anp2ff3+gOJJzRImtrtm/oa9tYLN0iTuINZ++EyVR/nRrg2fKYwvG/pXDofcrvvb0scEbX3NyW/COw=="],
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
-1
View File
@@ -34,7 +34,6 @@
"direction": "^2.0.1", "direction": "^2.0.1",
"event-iterator": "^2.0.0", "event-iterator": "^2.0.0",
"feed": "^5.1.0", "feed": "^5.1.0",
"fflate": "^0.8.3",
"flexsearch": "^0.8.212", "flexsearch": "^0.8.212",
"image-size": "^2.0.2", "image-size": "^2.0.2",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
+1 -3
View File
@@ -5,7 +5,6 @@ import rison from 'rison';
import type { SiteAPIToken } from '@gitbook/api'; import type { SiteAPIToken } from '@gitbook/api';
import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive'; import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive';
import { decompressAPIToken } from '@/lib/api-token-compression';
import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@/lib/context'; import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@/lib/context';
import { getDynamicCustomizationSettings } from '@/lib/customization'; import { getDynamicCustomizationSettings } from '@/lib/customization';
@@ -125,8 +124,7 @@ function getModeFromParams(mode: string): RouteParamMode {
export function getSiteURLDataFromParams(params: RouteLayoutParams): SiteURLData { export function getSiteURLDataFromParams(params: RouteLayoutParams): SiteURLData {
try { try {
const decoded = decodeURIComponent(params.siteData); const decoded = decodeURIComponent(params.siteData);
const data = rison.decode<SiteURLData>(decoded); return rison.decode(decoded);
return { ...data, apiToken: decompressAPIToken(data.apiToken) };
} catch (error) { } catch (error) {
console.error( console.error(
`Returning 404 after failing to decode site data ${params.siteData}: ${error}` `Returning 404 after failing to decode site data ${params.siteData}: ${error}`
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'bun:test';
import { es } from './es';
import { fr } from './fr';
import { isAvailableLanguage, languages, loadLanguage } from './index';
describe('regional language variants', () => {
it.each([
['fr-ca', fr],
['es-mx', es],
['es-419', es],
] as const)('loads %s with its own metadata and the base strings', async (locale, base) => {
expect(isAvailableLanguage(locale)).toBe(true);
const language = await loadLanguage(locale);
expect(language).toEqual({ ...base, ...languages[locale] });
expect(language.locale).toBe(locale);
});
});
@@ -25,6 +25,10 @@ const languageDefinitions = {
metadata: { locale: 'fr', language: 'Français', flag: '🇫🇷' }, metadata: { locale: 'fr', language: 'Français', flag: '🇫🇷' },
load: () => import('./fr').then((module) => module.fr), load: () => import('./fr').then((module) => module.fr),
}, },
'fr-ca': {
metadata: { locale: 'fr-ca', language: 'Français (Canada)', flag: '🇨🇦' },
load: () => import('./fr').then((module) => withMetadata(module.fr, 'fr-ca')),
},
de: { de: {
metadata: { locale: 'de', language: 'Deutsch', flag: '🇩🇪' }, metadata: { locale: 'de', language: 'Deutsch', flag: '🇩🇪' },
load: () => import('./de').then((module) => module.de), load: () => import('./de').then((module) => module.de),
@@ -33,6 +37,14 @@ const languageDefinitions = {
metadata: { locale: 'es', language: 'Español', flag: '🇪🇸' }, metadata: { locale: 'es', language: 'Español', flag: '🇪🇸' },
load: () => import('./es').then((module) => module.es), load: () => import('./es').then((module) => module.es),
}, },
'es-mx': {
metadata: { locale: 'es-mx', language: 'Español (México)', flag: '🇲🇽' },
load: () => import('./es').then((module) => withMetadata(module.es, 'es-mx')),
},
'es-419': {
metadata: { locale: 'es-419', language: 'Español (Latinoamérica)', flag: '🌎' },
load: () => import('./es').then((module) => withMetadata(module.es, 'es-419')),
},
it: { it: {
metadata: { locale: 'it', language: 'Italiano', flag: '🇮🇹' }, metadata: { locale: 'it', language: 'Italiano', flag: '🇮🇹' },
load: () => import('./it').then((module) => module.it), load: () => import('./it').then((module) => module.it),
@@ -177,6 +189,14 @@ const languageDefinitions = {
export type TranslationLocale = keyof typeof languageDefinitions; export type TranslationLocale = keyof typeof languageDefinitions;
// Regional variants reuse their base language's strings but keep their own locale, name and flag.
function withMetadata(
base: TranslationLanguage,
locale: 'fr-ca' | 'es-mx' | 'es-419'
): TranslationLanguage {
return { ...base, ...languageDefinitions[locale].metadata };
}
export const languages = Object.fromEntries( export const languages = Object.fromEntries(
Object.entries(languageDefinitions).map(([locale, definition]) => [locale, definition.metadata]) Object.entries(languageDefinitions).map(([locale, definition]) => [locale, definition.metadata])
) as { ) as {
@@ -1,88 +0,0 @@
import { describe, expect, it } from 'bun:test';
import jwt from 'jsonwebtoken';
import { inflateRawSync } from 'node:zlib';
import {
API_TOKEN_COMPRESSION_THRESHOLD,
COMPRESSED_API_TOKEN_PREFIX,
compressAPITokenIfNeeded,
decompressAPIToken,
} from './api-token-compression';
const ALPHANUM = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
function randomId() {
return Array.from({ length: 20 }, () => ALPHANUM[Math.floor(Math.random() * 62)]).join('');
}
function signToken(spaces: number, claims: Record<string, unknown> = {}) {
return jwt.sign(
{
kind: 'site',
organization: randomId(),
site: `site_${randomId()}`,
space: randomId(),
spaces: Array.from({ length: spaces }, randomId),
claims,
},
'secret'
);
}
describe('compressAPITokenIfNeeded', () => {
it('keeps tokens under the threshold unchanged', () => {
const token = signToken(10);
expect(token.length).toBeLessThanOrEqual(API_TOKEN_COMPRESSION_THRESHOLD);
expect(compressAPITokenIfNeeded(token)).toBe(token);
});
it('compresses large tokens and restores them exactly', () => {
const token = signToken(600, {
groups: Array.from({ length: 100 }, (_, i) => `group-${i}`),
});
expect(token.length).toBeGreaterThan(API_TOKEN_COMPRESSION_THRESHOLD);
const compressed = compressAPITokenIfNeeded(token);
expect(compressed.startsWith(COMPRESSED_API_TOKEN_PREFIX)).toBe(true);
expect(compressed.length).toBeLessThan(token.length * 0.85);
expect(decompressAPIToken(compressed)).toBe(token);
});
it('restores tokens with non-ASCII characters and dots in the payload', () => {
const token = signToken(600, { name: 'Zoë Ünal', 'a.b': 'é.ü.日本', email: 'z.u@ex.co' });
expect(decompressAPIToken(compressAPITokenIfNeeded(token))).toBe(token);
});
it('produces a stable output for the same token', () => {
const token = signToken(600);
expect(compressAPITokenIfNeeded(token)).toBe(compressAPITokenIfNeeded(token));
});
it('produces raw deflate data', () => {
const token = signToken(600);
const compressed = compressAPITokenIfNeeded(token);
const inflated = inflateRawSync(
Buffer.from(compressed.slice(COMPRESSED_API_TOKEN_PREFIX.length), 'base64url')
);
const [header, payload, signature] = token.split('.');
expect(inflated.toString()).toBe(
`${header}.${Buffer.from(payload!, 'base64url').toString()}.${signature}`
);
});
it('does not compress values that are not a JWT', () => {
const value = 'x'.repeat(API_TOKEN_COMPRESSION_THRESHOLD + 1);
expect(compressAPITokenIfNeeded(value)).toBe(value);
});
});
describe('decompressAPIToken', () => {
it('returns raw tokens unchanged', () => {
const token = signToken(10);
expect(decompressAPIToken(token)).toBe(token);
});
it('throws on corrupted compressed tokens', () => {
expect(() => decompressAPIToken(`${COMPRESSED_API_TOKEN_PREFIX}AAAA`)).toThrow();
});
});
@@ -1,94 +0,0 @@
import { deflateSync, inflateSync } from 'fflate';
/**
* Prefix marking a compressed API token. A JWT always starts with `eyJ` and `.` is not a
* base64url character, so it can't collide with a raw token.
*/
export const COMPRESSED_API_TOKEN_PREFIX = 'gbz1.';
/**
* Tokens above this length are compressed before being encoded in the rewritten URL,
* which is limited to 16KB. Smaller tokens are kept as-is so their URLs (and cache keys) don't change.
*/
export const API_TOKEN_COMPRESSION_THRESHOLD = 14 * 1024;
const DOT = 0x2e;
/**
* Compress an API token if it is too large to safely fit in the rewritten URL.
* The payload is decoded before compressing, as deflate does much better on JSON than on base64.
*/
export function compressAPITokenIfNeeded(token: string): string {
if (token.length <= API_TOKEN_COMPRESSION_THRESHOLD) {
return token;
}
const parts = token.split('.');
if (parts.length !== 3) {
return token;
}
const [header, payload, signature] = parts as [string, string, string];
const encoder = new TextEncoder();
const bytes = concatBytes([
encoder.encode(`${header}.`),
base64UrlToBytes(payload),
encoder.encode(`.${signature}`),
]);
return `${COMPRESSED_API_TOKEN_PREFIX}${bytesToBase64Url(deflateSync(bytes, { level: 9 }))}`;
}
/**
* Restore the original API token from a value produced by `compressAPITokenIfNeeded`.
* Values that are not compressed are returned as-is.
*/
export function decompressAPIToken(value: string): string {
if (!value.startsWith(COMPRESSED_API_TOKEN_PREFIX)) {
return value;
}
const bytes = inflateSync(base64UrlToBytes(value.slice(COMPRESSED_API_TOKEN_PREFIX.length)));
// Header and signature are base64url so they can't contain dots, but the payload JSON can.
const firstDot = bytes.indexOf(DOT);
const lastDot = bytes.lastIndexOf(DOT);
if (firstDot === -1 || firstDot === lastDot) {
throw new Error('Invalid compressed API token');
}
const decoder = new TextDecoder();
const header = decoder.decode(bytes.subarray(0, firstDot));
const payload = bytesToBase64Url(bytes.subarray(firstDot + 1, lastDot));
const signature = decoder.decode(bytes.subarray(lastDot + 1));
return `${header}.${payload}.${signature}`;
}
function concatBytes(chunks: Uint8Array[]): Uint8Array {
const result = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
// Implemented with atob/btoa as Buffer is not guaranteed in the edge runtime.
function bytesToBase64Url(bytes: Uint8Array): string {
let binary = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
}
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function base64UrlToBytes(value: string): Uint8Array {
const binary = atob(value.replace(/-/g, '+').replace(/_/g, '/'));
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
+43 -8
View File
@@ -1,10 +1,6 @@
/** import { isAIEnabled } from '@/components/utils/isAIChatEnabled';
* Describe the `ask` and `goal` query parameters of the ask endpoint, for agent-facing prompts. import type { GitBookSiteContext } from '@/lib/context';
*/ import { linkerWithMarkdownPages } from '@/lib/links';
export function renderAskParametersDescription(): string {
return `\`ask\` is the immediate question: it should be specific, self-contained, and written in natural language.
\`goal\` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with \`ask=how do I create an API token\`, a goal like \`build a script that syncs our docs to a CMS\` lets GitBook tailor the answer to that use case.`;
}
/** /**
* Render the "Querying This Documentation" section of the agent instructions. * Render the "Querying This Documentation" section of the agent instructions.
@@ -18,7 +14,46 @@ export function renderQueryingDocumentation(options: { pageUrl: string }): strin
GET ${pageUrl}?ask=<question>&goal=<user_goal> GET ${pageUrl}?ask=<question>&goal=<user_goal>
\`\`\` \`\`\`
${renderAskParametersDescription()} \`ask\` is the immediate question: it should be specific, self-contained, and written in natural language.
\`goal\` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with \`ask=how do I create an API token\`, a goal like \`automate deployments from our CI pipeline\` lets GitBook tailor the answer to that use case.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.`; The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.`;
} }
const AGENT_INSTRUCTIONS_INTRO = `# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.
## Querying This Documentation`;
/**
* Render the "Agent Instructions" of a markdown page (callers add the separator).
*/
export function renderPageAgentInstructions(options: { pageUrl: string }): string {
return `${AGENT_INSTRUCTIONS_INTRO}
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.
${renderQueryingDocumentation(options)}
Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
`;
}
/**
* Render the "Agent Instructions" of the site-wide llms.txt and llms-full.txt files.
* Returns `undefined` when AI is disabled for the site.
*/
export function renderSiteAgentInstructions(context: GitBookSiteContext): string | undefined {
if (!isAIEnabled(context.customization.ai.mode)) {
return undefined;
}
// The ask endpoint ignores the page path, so the top-level `index.md` works on every site.
const linker = linkerWithMarkdownPages(context.linker);
const pageUrl = linker.toAbsoluteURL(linker.toPathForPagePath({ path: 'index' }));
return `${AGENT_INSTRUCTIONS_INTRO}
This site has an agentic ask interface you may use to query the documentation dynamically by asking a question.
${renderQueryingDocumentation({ pageUrl })}
`;
}
+1 -3
View File
@@ -2,7 +2,6 @@ import { headers } from 'next/headers';
import { CustomizationDefaultThemeMode } from '@gitbook/api'; import { CustomizationDefaultThemeMode } from '@gitbook/api';
import { decompressAPIToken } from './api-token-compression';
import type { SiteURLData } from './context'; import type { SiteURLData } from './context';
export enum MiddlewareHeaders { export enum MiddlewareHeaders {
@@ -71,8 +70,7 @@ export async function getSiteURLDataFromMiddleware(): Promise<SiteURLData> {
); );
} }
const data: SiteURLData = JSON.parse(siteURLData); return JSON.parse(siteURLData);
return { ...data, apiToken: decompressAPIToken(data.apiToken) };
} }
/** /**
+2 -2
View File
@@ -22,7 +22,6 @@ import {
trackServerInsightsEvents, trackServerInsightsEvents,
} from './lib/tracking'; } from './lib/tracking';
import { AI_CATALOG_PATH, AI_CATALOG_WELL_KNOWN_PATH } from '@/lib/aiCatalog/paths'; import { AI_CATALOG_PATH, AI_CATALOG_WELL_KNOWN_PATH } from '@/lib/aiCatalog/paths';
import { compressAPITokenIfNeeded } from '@/lib/api-token-compression';
import { getAPITokenFromCookies, getAPITokenResponseCookies } from '@/lib/api-token-cookie'; import { getAPITokenFromCookies, getAPITokenResponseCookies } from '@/lib/api-token-cookie';
import { isChatGPTRequest } from '@/lib/chatgpt'; import { isChatGPTRequest } from '@/lib/chatgpt';
import { MAX_CHUNKED_COOKIE_LENGTH } from '@/lib/chunked-cookies'; import { MAX_CHUNKED_COOKIE_LENGTH } from '@/lib/chunked-cookies';
@@ -395,7 +394,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
revision: siteURLData.revision, revision: siteURLData.revision,
shareKey: siteURLData.shareKey, shareKey: siteURLData.shareKey,
preview: siteURLData.preview, preview: siteURLData.preview,
apiToken: compressAPITokenIfNeeded(siteURLData.apiToken), apiToken: siteURLData.apiToken,
imagesContextId: imagesContextId, imagesContextId: imagesContextId,
contextId: siteURLData.contextId, contextId: siteURLData.contextId,
isFallback: requestURL.searchParams.get('fallback') === 'true' ? true : undefined, isFallback: requestURL.searchParams.get('fallback') === 'true' ? true : undefined,
@@ -955,6 +954,7 @@ function encodePathInSiteContent(
{ {
type: 'ask_question', type: 'ask_question',
query: ask, query: ask,
goal: typeof goal === 'string' ? goal : undefined,
location: { location: {
displayContext: SiteInsightsDisplayContext.Server, displayContext: SiteInsightsDisplayContext.Server,
}, },
+7
View File
@@ -4,6 +4,7 @@ import pMap, { pMapIterable } from 'p-map';
import type { RevisionPageDocument, SiteSection, SiteSpace } from '@gitbook/api'; import type { RevisionPageDocument, SiteSection, SiteSpace } from '@gitbook/api';
import { renderSiteAgentInstructions } from '@/lib/ask-prompt';
import { import {
type GitBookSiteContext, type GitBookSiteContext,
checkIsRootSiteContext, checkIsRootSiteContext,
@@ -171,6 +172,12 @@ async function streamMarkdownPageEntries(
stream.enqueue(new TextEncoder().encode(`${header.join('\n')}\n\n`)); stream.enqueue(new TextEncoder().encode(`${header.join('\n')}\n\n`));
} }
// Rendered up front, on every part, since agents may only read the start of the file.
const agentInstructions = renderSiteAgentInstructions(context);
if (agentInstructions) {
stream.enqueue(new TextEncoder().encode(`${agentInstructions}\n---\n\n`));
}
// Process the pages // Process the pages
for await (const markdown of pMapIterable( for await (const markdown of pMapIterable(
pagesToProcess, pagesToProcess,
+5 -27
View File
@@ -4,12 +4,11 @@ import { toMarkdown } from 'mdast-util-to-markdown';
import type { SiteSection, SiteSpace } from '@gitbook/api'; import type { SiteSection, SiteSpace } from '@gitbook/api';
import { isAIEnabled } from '@/components/utils/isAIChatEnabled'; import { renderSiteAgentInstructions } from '@/lib/ask-prompt';
import { type GitBookSiteContext, checkIsRootSiteContext } from '@/lib/context'; import { type GitBookSiteContext, checkIsRootSiteContext } from '@/lib/context';
import { throwIfDataError } from '@/lib/data'; import { throwIfDataError } from '@/lib/data';
import { type GitBookLinker, linkerWithMarkdownPages } from '@/lib/links'; import { type GitBookLinker, linkerWithMarkdownPages } from '@/lib/links';
import { getMarkdownContentType } from '@/lib/markdown-content-type'; import { getMarkdownContentType } from '@/lib/markdown-content-type';
import { resolveFirstDocument } from '@/lib/pages';
import { type FlatPageEntry, getIndexablePages } from '@/lib/sitemap'; import { type FlatPageEntry, getIndexablePages } from '@/lib/sitemap';
import { import {
filterSiteSpacesByLocale, filterSiteSpacesByLocale,
@@ -49,7 +48,10 @@ export async function serveLLMsTxt(baseContext: GitBookSiteContext) {
bullet: '-', bullet: '-',
}); });
output += renderAskFooter(context); const agentInstructions = renderSiteAgentInstructions(context);
if (agentInstructions) {
output += `\n\n---\n\n${agentInstructions}`;
}
return new Response(output, { return new Response(output, {
headers: { headers: {
@@ -205,27 +207,3 @@ export async function getMarkdownForPagesTree(
}); });
return nodes; return nodes;
} }
function renderAskFooter(context: GitBookSiteContext) {
if (!isAIEnabled(context.customization.ai.mode)) {
return '';
}
return `\n\n---\n\n# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.
## Querying This Documentation
If you need additional information, you can query the documentation dynamically by asking a question.
Perform an HTTP GET request on a page URL with the \`ask\` query parameter:
\`\`\`
GET ${context.linker.toAbsoluteURL(
context.linker.toPathForPagePath({
path: resolveFirstDocument(context.revision.pages, [])?.page.path ?? 'index',
})
)}?ask=<question>
\`\`\`
The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.
Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
`;
}
+2 -11
View File
@@ -2,7 +2,7 @@ import type { RevisionPageDocument, RevisionPageGroup } from '@gitbook/api';
import { resolveMissingPagePath } from '@/components/SitePage/fetch'; import { resolveMissingPagePath } from '@/components/SitePage/fetch';
import { isAIEnabled } from '@/components/utils/isAIChatEnabled'; import { isAIEnabled } from '@/components/utils/isAIChatEnabled';
import { renderQueryingDocumentation } from '@/lib/ask-prompt'; import { renderPageAgentInstructions, renderQueryingDocumentation } from '@/lib/ask-prompt';
import type { GitBookSiteContext } from '@/lib/context'; import type { GitBookSiteContext } from '@/lib/context';
import { getExposableError } from '@/lib/data'; import { getExposableError } from '@/lib/data';
import { linkerWithMarkdownPages } from '@/lib/links'; import { linkerWithMarkdownPages } from '@/lib/links';
@@ -184,16 +184,7 @@ function renderAskFooter(
}) })
); );
return `\n\n---\n\n# Agent Instructions return `\n\n---\n\n${renderPageAgentInstructions({ pageUrl })}`;
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.
## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.
${renderQueryingDocumentation({ pageUrl })}
Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
`;
} }
/** /**