@stainless-api/docs 0.1.0-beta.136 → 0.1.0-beta.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/eslint-suppressions.json +0 -5
- package/package.json +13 -13
- package/plugin/components/RequestBuilder/index.tsx +0 -3
- package/plugin/globalJs/ai-dropdown-options.ts +15 -7
- package/plugin/index.ts +5 -1
- package/plugin/loadPluginConfig.ts +1 -1
- package/plugin/specs/generateSpec.ts +1 -3
- package/plugin/vendor/preview.worker.docs.js +15523 -27699
- package/shared/virtualModule.ts +0 -17
- package/stl-docs/aiChatExamples.ts +11 -75
- package/stl-docs/chat/docs-chat-handler.ts +4 -5
- package/stl-docs/chat/hook.ts +19 -9
- package/stl-docs/chat/schemas.ts +0 -43
- package/stl-docs/chat/ui/AiChat.tsx +11 -24
- package/stl-docs/chat/ui/components/ChatLog.tsx +0 -3
- package/stl-docs/chat/ui/components/MessageFeedback.tsx +1 -4
- package/stl-docs/chat/ui/components/ToolCall.tsx +20 -20
- package/stl-docs/chat/ui/types.ts +2 -2
- package/stl-docs/components/AiChatIsland.tsx +2 -6
- package/stl-docs/components/PageFrame.astro +1 -5
- package/stl-docs/fonts.ts +4 -4
- package/stl-docs/index.ts +33 -32
- package/stl-docs/loadStlDocsConfig.ts +2 -2
- package/virtual-module.d.ts +6 -1
- package/stl-docs/chat/stainless-handler/index.ts +0 -126
- package/stl-docs/chat/stream-util.ts +0 -16
- package/stl-docs/proseSearchIndexing.ts +0 -218
package/virtual-module.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ declare module 'virtual:stl-starlight-virtual-module' {
|
|
|
19
19
|
export const PROPERTY_SETTINGS: PropertySettingsType;
|
|
20
20
|
export const MIDDLEWARE: StlStarlightMiddleware;
|
|
21
21
|
export const ENABLE_CONTEXT_MENU: boolean;
|
|
22
|
+
export const CONTEXT_MENU_ENABLE_THIRD_PARTY: boolean;
|
|
22
23
|
export const STAINLESS_PROJECT: string | undefined;
|
|
23
24
|
export const LLMS_TXT_DESCRIPTION: string | null;
|
|
24
25
|
export const LLMS_TXT_DETAIL_THRESHOLD: number;
|
|
@@ -53,6 +54,7 @@ declare module 'virtual:stl-docs-virtual-module' {
|
|
|
53
54
|
export const API_REFERENCE_BASE_PATH: string;
|
|
54
55
|
export const ENABLE_PROSE_MARKDOWN_RENDERING: boolean;
|
|
55
56
|
export const ENABLE_CONTEXT_MENU: boolean;
|
|
57
|
+
export const CONTEXT_MENU_ENABLE_THIRD_PARTY: boolean;
|
|
56
58
|
export const RENDER_PAGE_DESCRIPTIONS: boolean;
|
|
57
59
|
export const LINK_GROUP_TITLES_TO_OVERVIEW_PAGES: boolean;
|
|
58
60
|
export const FONTS: {
|
|
@@ -63,7 +65,10 @@ declare module 'virtual:stl-docs-virtual-module' {
|
|
|
63
65
|
};
|
|
64
66
|
export const RENDER_CREDITS: boolean;
|
|
65
67
|
export const SITE_TITLE: string;
|
|
66
|
-
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
declare module 'virtual:stl-docs-ai-chat' {
|
|
71
|
+
export const AI_CHAT_HANDLER: import('./stl-docs/chat/docs-chat-handler').DocsChatHandler | undefined;
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
declare module 'virtual:stl-docs-ai-chat-examples' {
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import type { DocsLanguage } from '@stainless-api/docs-ui/routing';
|
|
2
|
-
import { JSONParser } from '@streamparser/json-whatwg';
|
|
3
|
-
import {
|
|
4
|
-
type RequestBody,
|
|
5
|
-
responseChunk,
|
|
6
|
-
type FeedbackRequestBody,
|
|
7
|
-
feedbackResponseBody,
|
|
8
|
-
type MetadataRequestBody,
|
|
9
|
-
metadataResponseBody,
|
|
10
|
-
} from '../schemas';
|
|
11
|
-
import { streamAsyncIterator } from '../stream-util';
|
|
12
|
-
import { DocsChatHandler } from '../docs-chat-handler';
|
|
13
|
-
|
|
14
|
-
const API_URL = new URL('https://app.stainless.com/api/');
|
|
15
|
-
const CHAT_ENDPOINT = new URL('ai/get-agentic-help', API_URL);
|
|
16
|
-
|
|
17
|
-
const FEEDBACK_ENDPOINT = (spanId: string) => new URL(`ai/agentic-help/${spanId}/score`, API_URL);
|
|
18
|
-
const METADATA_ENDPOINT = (spanId: string) => new URL(`ai/agentic-help/${spanId}/metadata`, API_URL);
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Identifier for tracking unique users in braintrust
|
|
22
|
-
*/
|
|
23
|
-
function getClientId() {
|
|
24
|
-
let clientId = localStorage.getItem('stainless-client-id');
|
|
25
|
-
if (!clientId) {
|
|
26
|
-
clientId = crypto.randomUUID();
|
|
27
|
-
localStorage.setItem('stainless-client-id', clientId);
|
|
28
|
-
}
|
|
29
|
-
return clientId;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Context on what the user is currently viewing to pass to the agent */
|
|
33
|
-
function getPageContext({ siteTitle }: { siteTitle: string | undefined }) {
|
|
34
|
-
const { href } = window.location;
|
|
35
|
-
const markdownUrl = `${href.replace(/\/$/, '')}/index.md`;
|
|
36
|
-
const pageTitle = document.querySelector('h1')?.textContent;
|
|
37
|
-
return [
|
|
38
|
-
`The user is viewing a documentation page${siteTitle ? ` for ${siteTitle}` : ''}.`,
|
|
39
|
-
`- Content URL: ${markdownUrl}`,
|
|
40
|
-
pageTitle && `- Page title: "${pageTitle}"`,
|
|
41
|
-
// TODO: include stainless path here? does the agent know how to use it?
|
|
42
|
-
// TODO: pass more of the page content into context without the agent having to retrieve it
|
|
43
|
-
]
|
|
44
|
-
.filter(Boolean)
|
|
45
|
-
.join('\n');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export class StainlessHandler implements DocsChatHandler {
|
|
49
|
-
constructor(
|
|
50
|
-
private language: DocsLanguage,
|
|
51
|
-
private siteTitle: string | undefined,
|
|
52
|
-
private project: string,
|
|
53
|
-
) {}
|
|
54
|
-
/**
|
|
55
|
-
* Stream chat response from the server
|
|
56
|
-
*/
|
|
57
|
-
async *generateResponse(
|
|
58
|
-
{
|
|
59
|
-
query,
|
|
60
|
-
priorMessages,
|
|
61
|
-
}: {
|
|
62
|
-
query: string;
|
|
63
|
-
priorMessages: NonNullable<RequestBody['additionalContext']>['prior_messages'];
|
|
64
|
-
},
|
|
65
|
-
abortSignal: AbortSignal,
|
|
66
|
-
) {
|
|
67
|
-
const res = await fetch(CHAT_ENDPOINT, {
|
|
68
|
-
method: 'POST',
|
|
69
|
-
headers: {
|
|
70
|
-
'Content-Type': 'application/json',
|
|
71
|
-
},
|
|
72
|
-
body: JSON.stringify({
|
|
73
|
-
query,
|
|
74
|
-
sdk: { project: this.project, language: this.language },
|
|
75
|
-
stream: true,
|
|
76
|
-
additionalContext: {
|
|
77
|
-
prior_messages: priorMessages,
|
|
78
|
-
intent: getPageContext({ siteTitle: this.siteTitle }),
|
|
79
|
-
},
|
|
80
|
-
browser_id: getClientId(),
|
|
81
|
-
} satisfies RequestBody),
|
|
82
|
-
|
|
83
|
-
signal: abortSignal,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
if (!res.ok || !res.body) throw new Error(`Chat request failed with status ${res.status}`);
|
|
87
|
-
|
|
88
|
-
const parser = new JSONParser({ separator: '\n', paths: ['$'] });
|
|
89
|
-
for await (const chunk of streamAsyncIterator(res.body.pipeThrough(parser))) {
|
|
90
|
-
const chunkParsed = responseChunk.safeParse(chunk.value);
|
|
91
|
-
if (chunkParsed.success) yield chunkParsed.data;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Attach a score to a response
|
|
97
|
-
*/
|
|
98
|
-
async onRate(spanId: string, score: 0 | 1) {
|
|
99
|
-
const res = await fetch(FEEDBACK_ENDPOINT(spanId), {
|
|
100
|
-
method: 'PUT',
|
|
101
|
-
headers: {
|
|
102
|
-
'Content-Type': 'application/json',
|
|
103
|
-
},
|
|
104
|
-
body: JSON.stringify({ score } satisfies FeedbackRequestBody),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
if (!res.ok) throw new Error(`Feedback request failed with status ${res.status}`);
|
|
108
|
-
return feedbackResponseBody.parse(await res.json());
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Attach metadata to a response
|
|
113
|
-
*/
|
|
114
|
-
async onAssignMetadata(spanId: string, metadata: Record<string, string>) {
|
|
115
|
-
const res = await fetch(METADATA_ENDPOINT(spanId), {
|
|
116
|
-
method: 'PUT',
|
|
117
|
-
headers: {
|
|
118
|
-
'Content-Type': 'application/json',
|
|
119
|
-
},
|
|
120
|
-
body: JSON.stringify({ metadata } satisfies MetadataRequestBody),
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
if (!res.ok) throw new Error(`Metadata request failed with status ${res.status}`);
|
|
124
|
-
return metadataResponseBody.parse(await res.json());
|
|
125
|
-
}
|
|
126
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* https://jakearchibald.com/2017/async-iterators-and-generators/#making-streams-iterate
|
|
3
|
-
* safari does not yet support consuming ReadableStream as AsyncIterable
|
|
4
|
-
*/
|
|
5
|
-
export async function* streamAsyncIterator<T>(stream: ReadableStream<T>) {
|
|
6
|
-
const reader = stream.getReader();
|
|
7
|
-
try {
|
|
8
|
-
while (true) {
|
|
9
|
-
const { done, value } = await reader.read();
|
|
10
|
-
if (done) return;
|
|
11
|
-
yield value;
|
|
12
|
-
}
|
|
13
|
-
} finally {
|
|
14
|
-
reader.releaseLock();
|
|
15
|
-
}
|
|
16
|
-
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import type { AstroIntegration } from 'astro';
|
|
2
|
-
import { readFile } from 'fs/promises';
|
|
3
|
-
import { getProsePages } from '../shared/getProsePages';
|
|
4
|
-
import { getSharedLogger } from '../shared/getSharedLogger';
|
|
5
|
-
import { bold } from '../shared/terminalUtils';
|
|
6
|
-
import * as cheerio from 'cheerio';
|
|
7
|
-
import { buildProseIndex } from '@stainless-api/docs-search/providers/algolia';
|
|
8
|
-
|
|
9
|
-
class SectionContext {
|
|
10
|
-
headers: { level: number; text: string }[] = [];
|
|
11
|
-
headerId: string | undefined;
|
|
12
|
-
headerTag: string | undefined;
|
|
13
|
-
headerText: string | undefined;
|
|
14
|
-
hasContent = false;
|
|
15
|
-
|
|
16
|
-
get(): string | undefined {
|
|
17
|
-
if (this.headers.length === 0) return;
|
|
18
|
-
return this.headers.map((h) => h.text).join(' > ');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
header({ id, tag, text }: { id: string; tag: string; text: string }) {
|
|
22
|
-
const level = getHeaderLevel(tag);
|
|
23
|
-
if (level > 0) {
|
|
24
|
-
while (this.headers.length > 0 && this.headers[this.headers.length - 1]!.level >= level) {
|
|
25
|
-
this.headers.pop();
|
|
26
|
-
}
|
|
27
|
-
this.headers.push({ level, text });
|
|
28
|
-
}
|
|
29
|
-
this.headerId = id;
|
|
30
|
-
this.headerTag = tag;
|
|
31
|
-
this.headerText = text;
|
|
32
|
-
this.hasContent = false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function slugify(text: string): string {
|
|
37
|
-
return text
|
|
38
|
-
.toLowerCase()
|
|
39
|
-
.replace(/`/g, '')
|
|
40
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
41
|
-
.replace(/^-|-$/g, '');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function getHeaderLevel(tag: string): number {
|
|
45
|
-
const match = tag.match(/^h(\d)$/);
|
|
46
|
-
return match ? parseInt(match[1]!, 10) : 0;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Chunking configuration
|
|
50
|
-
// We target 64-256 tokens per chunk, using ~1.3 tokens/word for English text
|
|
51
|
-
const TOKENS_PER_WORD = 1.3;
|
|
52
|
-
const MIN_TOKENS = 64;
|
|
53
|
-
const MAX_TOKENS = 256;
|
|
54
|
-
const MIN_WORDS = Math.floor(MIN_TOKENS / TOKENS_PER_WORD); // ~49 words
|
|
55
|
-
const MAX_WORDS = Math.floor(MAX_TOKENS / TOKENS_PER_WORD); // ~197 words
|
|
56
|
-
const SENTENCE_BREAK_WORDS = Math.floor((MAX_TOKENS * 0.875) / TOKENS_PER_WORD); // ~172 words
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Chunks text content into segments of 64-256 tokens using word-based boundaries.
|
|
60
|
-
* Prefers breaking at sentence endings for natural chunk boundaries.
|
|
61
|
-
*/
|
|
62
|
-
function chunkTextByWords(text: string): string[] {
|
|
63
|
-
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
64
|
-
|
|
65
|
-
if (words.length <= MAX_WORDS) {
|
|
66
|
-
return words.length > 0 ? [words.join(' ')] : [];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const chunks: string[] = [];
|
|
70
|
-
let currentChunk: string[] = [];
|
|
71
|
-
|
|
72
|
-
for (const word of words) {
|
|
73
|
-
currentChunk.push(word);
|
|
74
|
-
|
|
75
|
-
// Force break at max words
|
|
76
|
-
if (currentChunk.length >= MAX_WORDS) {
|
|
77
|
-
chunks.push(currentChunk.join(' '));
|
|
78
|
-
currentChunk = [];
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Prefer breaking at sentence boundaries after threshold
|
|
83
|
-
if (currentChunk.length >= SENTENCE_BREAK_WORDS && /[.!?]["']?$/.test(word)) {
|
|
84
|
-
chunks.push(currentChunk.join(' '));
|
|
85
|
-
currentChunk = [];
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (currentChunk.length > 0) {
|
|
90
|
-
if (currentChunk.length < MIN_WORDS && chunks.length > 0) {
|
|
91
|
-
const lastChunk = chunks[chunks.length - 1]!;
|
|
92
|
-
const mergedWords = lastChunk.split(/\s+/).length + currentChunk.length;
|
|
93
|
-
if (mergedWords <= MAX_WORDS) {
|
|
94
|
-
chunks[chunks.length - 1] = lastChunk + ' ' + currentChunk.join(' ');
|
|
95
|
-
} else {
|
|
96
|
-
chunks.push(currentChunk.join(' '));
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
chunks.push(currentChunk.join(' '));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return chunks;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
type IndexEntry = {
|
|
107
|
-
chunk: { id: string; index: number; total: number };
|
|
108
|
-
id: string;
|
|
109
|
-
tag: string;
|
|
110
|
-
content: string;
|
|
111
|
-
language?: string;
|
|
112
|
-
sectionContext?: string;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const DEFAULT_ROOT = 'main';
|
|
116
|
-
const DEFAULT_PATTERN = 'h1, h2, h3, h4, h5, h6, p, li, pre code';
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Indexes HTML content for search, with section context and code language extraction.
|
|
120
|
-
*
|
|
121
|
-
* Features:
|
|
122
|
-
* - Tracks header hierarchy to prepend section context (e.g., "Guide > Setup: ...")
|
|
123
|
-
* - Extracts language metadata from code blocks (class="language-js")
|
|
124
|
-
* - Uses word-based chunking with sentence boundary detection
|
|
125
|
-
*/
|
|
126
|
-
function* indexHTML(content: string, root = DEFAULT_ROOT, pattern = DEFAULT_PATTERN): Generator<IndexEntry> {
|
|
127
|
-
const $ = cheerio.load(content);
|
|
128
|
-
const matches = $(root).find(pattern);
|
|
129
|
-
|
|
130
|
-
const ctx = new SectionContext();
|
|
131
|
-
|
|
132
|
-
for (const match of matches) {
|
|
133
|
-
const tagName = match.tagName.toLowerCase();
|
|
134
|
-
const rawText = $(match).text().trim();
|
|
135
|
-
|
|
136
|
-
if (getHeaderLevel(tagName) > 0) {
|
|
137
|
-
ctx.header({ id: $(match).attr('id') ?? slugify(rawText), tag: tagName, text: rawText });
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Check if this is a code block and extract language
|
|
142
|
-
const isCode = tagName === 'code' && $(match).parent().is('pre');
|
|
143
|
-
let language: string | undefined;
|
|
144
|
-
if (isCode) {
|
|
145
|
-
const classes = $(match).attr('class') || '';
|
|
146
|
-
const langMatch = classes.match(/(?:language-|lang-)([a-zA-Z0-9+-]+)/);
|
|
147
|
-
language = langMatch ? langMatch[1] : undefined;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Build content with section context
|
|
151
|
-
const sectionContext = ctx.get();
|
|
152
|
-
const chunks = chunkTextByWords(rawText);
|
|
153
|
-
const chunkId = crypto.randomUUID();
|
|
154
|
-
|
|
155
|
-
for (const [chunkN, chunkText] of chunks.entries()) {
|
|
156
|
-
yield {
|
|
157
|
-
id: ctx.headerId ?? $(match).attr('id') ?? chunkId,
|
|
158
|
-
tag: isCode ? 'code' : tagName,
|
|
159
|
-
content: chunkText,
|
|
160
|
-
...(sectionContext ? { sectionContext } : {}),
|
|
161
|
-
...(language && { language }),
|
|
162
|
-
chunk: {
|
|
163
|
-
id: chunkId,
|
|
164
|
-
index: chunkN,
|
|
165
|
-
total: chunks.length,
|
|
166
|
-
},
|
|
167
|
-
};
|
|
168
|
-
ctx.hasContent = true;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function stainlessDocsAlgoliaProseIndexing({
|
|
174
|
-
apiReferenceBasePath,
|
|
175
|
-
}: {
|
|
176
|
-
apiReferenceBasePath: string | null;
|
|
177
|
-
}): AstroIntegration {
|
|
178
|
-
return {
|
|
179
|
-
name: 'stl-docs-prose-indexing',
|
|
180
|
-
hooks: {
|
|
181
|
-
'astro:build:done': async ({ logger: localLogger, dir }) => {
|
|
182
|
-
const logger = getSharedLogger({ fallback: localLogger });
|
|
183
|
-
const outputBasePath = dir.pathname;
|
|
184
|
-
|
|
185
|
-
const {
|
|
186
|
-
PUBLIC_ALGOLIA_APP_ID: appId,
|
|
187
|
-
PUBLIC_ALGOLIA_INDEX: indexName,
|
|
188
|
-
PRIVATE_ALGOLIA_WRITE_KEY: algoliaWriteKey,
|
|
189
|
-
} = process.env;
|
|
190
|
-
|
|
191
|
-
if (!appId || !indexName || !algoliaWriteKey) {
|
|
192
|
-
logger.info('Skipping algolia indexing due to missing environment variables');
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const pagesToRender = await getProsePages({ apiReferenceBasePath, outputBasePath });
|
|
197
|
-
logger.info(bold(`Indexing ${pagesToRender.length} prose pages for algolia search`));
|
|
198
|
-
|
|
199
|
-
const objects = [];
|
|
200
|
-
for (const absHtmlPath of pagesToRender) {
|
|
201
|
-
const content = await readFile(absHtmlPath, 'utf-8');
|
|
202
|
-
const idx = indexHTML(content);
|
|
203
|
-
for (const entry of idx)
|
|
204
|
-
objects.push({
|
|
205
|
-
...entry,
|
|
206
|
-
source: absHtmlPath.slice(outputBasePath.length),
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
await buildProseIndex(appId, `${indexName}-prose`, algoliaWriteKey, objects);
|
|
212
|
-
} catch (err) {
|
|
213
|
-
logger.error(`Failed to index prose content: ${err}`);
|
|
214
|
-
}
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
}
|