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
9 changed files with 108 additions and 46 deletions
+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.
@@ -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 {
+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
View File
@@ -954,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.
`;
} }
/** /**