@stainless-api/docs-ui 0.1.0-beta.13 → 0.1.0-beta.15
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/package.json +15 -7
- package/src/components/breadcrumbs.tsx +1 -1
- package/src/components/chat.tsx +18 -15
- package/src/components/method.tsx +8 -9
- package/src/components/overview.tsx +26 -12
- package/src/components/primitives.tsx +36 -19
- package/src/components/properties.tsx +4 -2
- package/src/components/scripts/dropdown.ts +1 -1
- package/src/components/sdk.tsx +25 -20
- package/src/components/sidebar.tsx +3 -3
- package/src/components/snippets.tsx +26 -9
- package/src/contexts/component-generics.tsx +10 -15
- package/src/contexts/docs.tsx +15 -4
- package/src/contexts/index.tsx +8 -5
- package/src/contexts/markdown.tsx +7 -6
- package/src/contexts/search.tsx +4 -5
- package/src/hooks/use-strict-context.tsx +16 -0
- package/src/languages/go.tsx +3 -3
- package/src/languages/http.tsx +31 -23
- package/src/languages/index.ts +7 -7
- package/src/languages/java.tsx +4 -4
- package/src/languages/python.tsx +12 -9
- package/src/languages/ruby.tsx +20 -13
- package/src/languages/typescript.tsx +18 -12
- package/src/markdown/index.ts +17 -12
- package/src/markdown/utils.ts +6 -3
- package/src/routing.ts +9 -9
- package/src/search/form.tsx +26 -24
- package/src/search/indexer.ts +17 -15
- package/src/search/mcp.ts +108 -16
- package/src/search/printer.tsx +1 -1
- package/src/search/providers/algolia.ts +5 -5
- package/src/search/providers/fuse.ts +4 -4
- package/src/search/providers/pagefind.ts +1 -1
- package/src/search/providers/walker.ts +5 -3
- package/src/search/results.tsx +7 -6
- package/src/search/types.ts +2 -2
- package/src/styles/main.css +2 -1
- package/src/utils.ts +9 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import
|
|
2
|
+
import { TSAST } from '@stainless/sdk-json';
|
|
3
3
|
import { useDeclaration, useLanguage, useLanguageComponents, useSpec } from '../contexts';
|
|
4
4
|
import { useComponents } from '../contexts/use-components';
|
|
5
5
|
import style from '../style';
|
|
@@ -13,7 +13,7 @@ const ComplexTypes: Record<string, string> = {
|
|
|
13
13
|
TSTypeArray: 'array',
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
const constStyle = {
|
|
16
|
+
const constStyle: Record<string, string> = {
|
|
17
17
|
string: style.LiteralString,
|
|
18
18
|
number: style.LiteralNumeric,
|
|
19
19
|
boolean: style.LiteralBoolean,
|
|
@@ -66,17 +66,23 @@ function TypeParams({ params }: { params?: TSAST.TSTypeParameter[] }) {
|
|
|
66
66
|
function TypePreview({ path }: { path: string }) {
|
|
67
67
|
const spec = useSpec();
|
|
68
68
|
const language = useLanguage();
|
|
69
|
-
const decl = useDeclaration(path);
|
|
69
|
+
const decl = useDeclaration(path, false);
|
|
70
70
|
const { Join } = useComponents();
|
|
71
71
|
|
|
72
|
-
if (
|
|
72
|
+
if (
|
|
73
|
+
!(decl && 'children' in decl && decl.children && decl.children.length > 0) ||
|
|
74
|
+
(decl && 'type' in decl && 'kind' in decl['type'] && decl['type']['kind'] === 'TSTypeUnion')
|
|
75
|
+
)
|
|
73
76
|
return;
|
|
74
77
|
|
|
75
|
-
const items = decl.children.map((prop, key) =>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const items = decl.children.map((prop, key) => {
|
|
79
|
+
const p = spec?.decls?.[language]?.[prop];
|
|
80
|
+
return (
|
|
81
|
+
<span key={key} className={style.TypePropertyName}>
|
|
82
|
+
<span className={style.TextIdentifier}>{p && 'key' in p ? p['key'] : null}</span>
|
|
83
|
+
</span>
|
|
84
|
+
);
|
|
85
|
+
});
|
|
80
86
|
|
|
81
87
|
return (
|
|
82
88
|
<span className={style.TypePreview} data-stldocs-type-preview="properties">
|
|
@@ -146,7 +152,7 @@ export function Type({ type }: TypeProps) {
|
|
|
146
152
|
|
|
147
153
|
return (
|
|
148
154
|
<span className={style.Type}>
|
|
149
|
-
<SDKReference stainlessPath={type.$ref}>{name}</SDKReference>
|
|
155
|
+
<SDKReference stainlessPath={type.$ref!}>{name}</SDKReference>
|
|
150
156
|
{params && params.length > 0 && (
|
|
151
157
|
<>
|
|
152
158
|
<span className={style.TypeBracket}>{'<'}</span>
|
|
@@ -156,7 +162,7 @@ export function Type({ type }: TypeProps) {
|
|
|
156
162
|
<span className={style.TypeBracket}>{'>'}</span>
|
|
157
163
|
</>
|
|
158
164
|
)}
|
|
159
|
-
<TypePreview path={type.$ref} />
|
|
165
|
+
<TypePreview path={type.$ref!} />
|
|
160
166
|
</span>
|
|
161
167
|
);
|
|
162
168
|
}
|
|
@@ -274,7 +280,7 @@ export function MethodSignature({ decl }: MethodSignatureProps) {
|
|
|
274
280
|
|
|
275
281
|
export type PropertyProps = {
|
|
276
282
|
decl: TSAST.TSDeclaration;
|
|
277
|
-
children
|
|
283
|
+
children: PropertyFn;
|
|
278
284
|
};
|
|
279
285
|
|
|
280
286
|
export function Property({ decl, children }: PropertyProps) {
|
package/src/markdown/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import Markdoc from '@markdoc/markdoc';
|
|
|
2
2
|
import * as md from './md';
|
|
3
3
|
import { EnvironmentType, getDecl, getSnippet, stripMarkup } from './utils';
|
|
4
4
|
import * as printer from '../search/printer';
|
|
5
|
-
import type * as SDKJSON from '
|
|
5
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
6
6
|
import type { Node } from '@markdoc/markdoc';
|
|
7
7
|
|
|
8
8
|
export function declaration(env: EnvironmentType, decl: SDKJSON.DeclarationNode) {
|
|
@@ -15,20 +15,20 @@ function renderChildren(env: EnvironmentType, children: SDKJSON.ID[], nesting: s
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function renderDecl(env: EnvironmentType, path: string, nesting: string[] = []) {
|
|
18
|
-
const decl = getDecl(env, path)
|
|
18
|
+
const decl = getDecl(env, path)!;
|
|
19
19
|
const item = md.item(declaration(env, decl));
|
|
20
20
|
|
|
21
|
-
const hasChildren = 'children' in decl && decl.children.length > 0;
|
|
22
|
-
const showModelProps = !decl['modelPath'] || env.options.includeModelProperties;
|
|
21
|
+
const hasChildren = 'children' in decl && decl.children && decl.children.length > 0;
|
|
22
|
+
const showModelProps = !('modelPath' in decl && decl['modelPath']) || env.options.includeModelProperties;
|
|
23
23
|
|
|
24
24
|
if ('docstring' in decl && decl.docstring) item.children.push(...md.parse(decl.docstring));
|
|
25
25
|
if (hasChildren && showModelProps && !nesting.includes(path))
|
|
26
|
-
item.push(renderChildren(env, decl.children, [...nesting, path]));
|
|
26
|
+
item.push(renderChildren(env, decl.children ?? [], [...nesting, path]));
|
|
27
27
|
|
|
28
28
|
return item;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function renderMethod(env: EnvironmentType, method: SDKJSON.Method) {
|
|
31
|
+
function renderMethod(env: EnvironmentType, method: SDKJSON.Method): Node[] {
|
|
32
32
|
const decl = getDecl(env, method.stainlessPath);
|
|
33
33
|
|
|
34
34
|
if (!decl)
|
|
@@ -41,11 +41,11 @@ function renderMethod(env: EnvironmentType, method: SDKJSON.Method) {
|
|
|
41
41
|
];
|
|
42
42
|
|
|
43
43
|
const signature = printer.methodSignature(env.language, decl);
|
|
44
|
-
const [httpMethod, endpoint] = method.endpoint.split(' ');
|
|
44
|
+
const [httpMethod, endpoint] = method.endpoint.split(' ') as [string, string];
|
|
45
45
|
|
|
46
46
|
const output = [
|
|
47
47
|
md.heading(2, method.title),
|
|
48
|
-
env.language === 'http' ?
|
|
48
|
+
...(env.language === 'http' ? [] : [md.paragraph(md.code(stripMarkup(signature)))]),
|
|
49
49
|
md.paragraph(md.strong(md.text(httpMethod)), md.text(' '), md.code(endpoint)),
|
|
50
50
|
];
|
|
51
51
|
|
|
@@ -54,7 +54,7 @@ function renderMethod(env: EnvironmentType, method: SDKJSON.Method) {
|
|
|
54
54
|
if ('paramsChildren' in decl && Array.isArray(decl.paramsChildren) && decl.paramsChildren.length > 0)
|
|
55
55
|
output.push(md.heading(3, 'Parameters'), renderChildren(env, decl.paramsChildren));
|
|
56
56
|
|
|
57
|
-
if ('responseChildren' in decl && decl.responseChildren.length > 0)
|
|
57
|
+
if ('responseChildren' in decl && decl.responseChildren && decl.responseChildren.length > 0)
|
|
58
58
|
output.push(md.heading(3, 'Returns'), renderChildren(env, decl.responseChildren));
|
|
59
59
|
|
|
60
60
|
const snippet = getSnippet(env, method.stainlessPath);
|
|
@@ -67,7 +67,7 @@ function renderModel(env: EnvironmentType, model: SDKJSON.Model) {
|
|
|
67
67
|
return [md.heading(3, model.title), md.list(renderDecl(env, `${model.stainlessPath} > (schema)`))];
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function renderResource(env: EnvironmentType, resource: SDKJSON.Resource) {
|
|
70
|
+
function renderResource(env: EnvironmentType, resource: SDKJSON.Resource): Node[] {
|
|
71
71
|
const methods = Object.values(resource.methods)
|
|
72
72
|
.filter((method) => getDecl(env, method.stainlessPath))
|
|
73
73
|
.flatMap((method) => renderMethod(env, method));
|
|
@@ -82,11 +82,16 @@ function renderResource(env: EnvironmentType, resource: SDKJSON.Resource) {
|
|
|
82
82
|
|
|
83
83
|
if (!env.options.renderNestedResources) return doc;
|
|
84
84
|
|
|
85
|
-
const children = Object.values(resource.subresources).
|
|
85
|
+
const children = Object.values(resource.subresources ?? {}).flatMap((resource) =>
|
|
86
|
+
renderResource(env, resource),
|
|
87
|
+
);
|
|
86
88
|
return [...doc, ...children];
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
export function render(
|
|
91
|
+
export function render(
|
|
92
|
+
env: EnvironmentType,
|
|
93
|
+
node: SDKJSON.Resource | SDKJSON.Method | SDKJSON.Model,
|
|
94
|
+
): Node[] {
|
|
90
95
|
switch (node.kind) {
|
|
91
96
|
case 'resource':
|
|
92
97
|
return renderResource(env, node);
|
package/src/markdown/utils.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Parser } from 'htmlparser2';
|
|
2
2
|
import type { DocsLanguage } from '../routing';
|
|
3
|
-
import type * as SDKJSON from '
|
|
3
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
4
4
|
import type { TransformRequestSnippetFn } from '../components/sdk';
|
|
5
5
|
|
|
6
6
|
export type EnvironmentType = {
|
|
@@ -19,7 +19,10 @@ export function getDecl(env: EnvironmentType, path: string) {
|
|
|
19
19
|
const decl = env.spec?.decls?.[env.language]?.[path];
|
|
20
20
|
|
|
21
21
|
if (decl?.kind?.endsWith('Reference')) {
|
|
22
|
-
const refId =
|
|
22
|
+
const refId =
|
|
23
|
+
'type' in decl && typeof decl['type'] === 'object' && '$ref' in decl['type']
|
|
24
|
+
? decl['type']['$ref']
|
|
25
|
+
: null;
|
|
23
26
|
if (refId === path) return decl;
|
|
24
27
|
if (refId) return getDecl(env, refId);
|
|
25
28
|
}
|
|
@@ -28,7 +31,7 @@ export function getDecl(env: EnvironmentType, path: string) {
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export function getSnippet(env: EnvironmentType, path: string) {
|
|
31
|
-
let snippet = env.spec?.snippets?.[`${env.language}.default`]?.[path];
|
|
34
|
+
let snippet = env.spec?.snippets?.[`${env.language}.default` as SDKJSON.SnippetLanguage]?.[path];
|
|
32
35
|
if (typeof snippet === 'string' && env.transforms?.transformRequestSnippet) {
|
|
33
36
|
snippet = env.transforms.transformRequestSnippet({ snippet, language: env.language });
|
|
34
37
|
}
|
package/src/routing.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type * as SDKJSON from '
|
|
1
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
2
2
|
|
|
3
3
|
export const Languages = [
|
|
4
4
|
'http',
|
|
@@ -56,10 +56,10 @@ export type ParsedStainlessPath = ReturnType<typeof parseStainlessPath>;
|
|
|
56
56
|
export function parseStainlessPath(stainlessPath: string) {
|
|
57
57
|
const match = stainlessPath.match(StainlessPathPattern);
|
|
58
58
|
|
|
59
|
-
if (!match) return null;
|
|
59
|
+
if (!match?.groups) return null;
|
|
60
60
|
|
|
61
61
|
return {
|
|
62
|
-
resource: match.groups.resource
|
|
62
|
+
resource: match.groups.resource?.split('.') ?? null,
|
|
63
63
|
method: match.groups.method ?? null,
|
|
64
64
|
model: match.groups.model ?? null,
|
|
65
65
|
routable: match.groups.model ? match[1] : match[0],
|
|
@@ -72,7 +72,7 @@ export function trimStainlessPath(stainlessPath: string) {
|
|
|
72
72
|
|
|
73
73
|
export function getResource(stainlessPath: string) {
|
|
74
74
|
const parsed = parseStainlessPath(stainlessPath);
|
|
75
|
-
return parsed?.resource[0];
|
|
75
|
+
return parsed?.resource?.[0];
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export function parseRoute(
|
|
@@ -119,7 +119,7 @@ export function generateRoute(basePath: string, language: string, stainlessPath:
|
|
|
119
119
|
const path = [basePath.endsWith('/') ? basePath.slice(0, -1) : basePath];
|
|
120
120
|
if (language && language !== DefaultLanguage) path.push(language);
|
|
121
121
|
|
|
122
|
-
const resources = parsedPath.resource
|
|
122
|
+
const resources = parsedPath.resource!.flatMap((name, index) => [
|
|
123
123
|
index > 0 ? 'subresources' : 'resources',
|
|
124
124
|
name,
|
|
125
125
|
]);
|
|
@@ -130,7 +130,7 @@ export function generateRoute(basePath: string, language: string, stainlessPath:
|
|
|
130
130
|
|
|
131
131
|
if (parsedPath.method) path.push('methods', parsedPath.method);
|
|
132
132
|
|
|
133
|
-
return stainlessPath.length > parsedPath.routable
|
|
133
|
+
return stainlessPath.length > parsedPath.routable!.length
|
|
134
134
|
? `${path.join('/')}#${encodeURIComponent(stainlessPath)}`
|
|
135
135
|
: path.join('/');
|
|
136
136
|
}
|
|
@@ -203,7 +203,7 @@ export function generateRouteList({
|
|
|
203
203
|
|
|
204
204
|
type ResourceOrMethod = SDKJSON.Resource | SDKJSON.Method;
|
|
205
205
|
|
|
206
|
-
export function findNavigationPath(items: ResourceOrMethod[], target: string) {
|
|
206
|
+
export function findNavigationPath(items: ResourceOrMethod[], target: string): string[] | undefined {
|
|
207
207
|
for (const item of Object.values(items)) {
|
|
208
208
|
if (item.stainlessPath === target) return [item.stainlessPath];
|
|
209
209
|
if (item.kind === 'http_method') continue;
|
|
@@ -217,9 +217,9 @@ export function findNavigationPath(items: ResourceOrMethod[], target: string) {
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
export function expandToElement(el: HTMLElement) {
|
|
220
|
+
export function expandToElement(el: HTMLElement | null) {
|
|
221
221
|
while (el) {
|
|
222
|
-
if (el
|
|
222
|
+
if (el instanceof HTMLDetailsElement) el.open = true;
|
|
223
223
|
el = el.parentElement;
|
|
224
224
|
}
|
|
225
225
|
}
|
package/src/search/form.tsx
CHANGED
|
@@ -14,36 +14,36 @@ export function SearchForm() {
|
|
|
14
14
|
const language = useLanguage();
|
|
15
15
|
const { onSelect, pageFind } = useSearchContext();
|
|
16
16
|
|
|
17
|
-
const [results, setResults] = React.useState<ResultData>(null);
|
|
17
|
+
const [results, setResults] = React.useState<ResultData>(null!);
|
|
18
18
|
const [filterKind, setFilterKind] = React.useState<QueryKindsType>('all');
|
|
19
19
|
const [searchQuery, setSearchQuery] = React.useState<string>('');
|
|
20
20
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
21
21
|
|
|
22
|
-
async function performSearch() {
|
|
23
|
-
const guideLimit = filterKind === 'guide' ? 25 : 5;
|
|
24
|
-
const kind = ['all', 'guide'].includes(filterKind) ? undefined : filterKind;
|
|
25
|
-
|
|
26
|
-
const [guideResults, apiResults] = await Promise.all([
|
|
27
|
-
pageFind ? guideSearch(pageFind, searchQuery, guideLimit) : [],
|
|
28
|
-
search({ query: searchQuery, kind, language }),
|
|
29
|
-
]);
|
|
30
|
-
|
|
31
|
-
setResults({
|
|
32
|
-
items: filterKind === 'guide' ? guideResults : [...guideResults, ...apiResults.hits],
|
|
33
|
-
counts: {
|
|
34
|
-
...apiResults.facets?.['kind'],
|
|
35
|
-
guide: guideResults.length,
|
|
36
|
-
all: apiResults.nbHits,
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
22
|
function clearInput() {
|
|
42
23
|
setSearchQuery('');
|
|
43
24
|
inputRef?.current?.focus();
|
|
44
25
|
}
|
|
45
26
|
|
|
46
|
-
React.useEffect(() =>
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
(async function performSearch() {
|
|
29
|
+
const guideLimit = filterKind === 'guide' ? 25 : 5;
|
|
30
|
+
const kind = ['all', 'guide'].includes(filterKind) ? undefined : filterKind;
|
|
31
|
+
|
|
32
|
+
const [guideResults, apiResults] = await Promise.all([
|
|
33
|
+
pageFind ? guideSearch(pageFind, searchQuery, guideLimit) : [],
|
|
34
|
+
search({ query: searchQuery, kind, language }),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
setResults({
|
|
38
|
+
items: filterKind === 'guide' ? guideResults : [...guideResults, ...(apiResults?.hits ?? [])],
|
|
39
|
+
counts: {
|
|
40
|
+
...apiResults?.facets?.['kind'],
|
|
41
|
+
guide: guideResults.length,
|
|
42
|
+
all: apiResults?.nbHits,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
})();
|
|
46
|
+
}, [searchQuery, filterKind, language, pageFind, search]);
|
|
47
47
|
|
|
48
48
|
return (
|
|
49
49
|
<div className={style.SearchForm}>
|
|
@@ -72,7 +72,9 @@ export function SearchForm() {
|
|
|
72
72
|
itemDelegate={(item) =>
|
|
73
73
|
'kind' in item ? <SearchResult result={item} /> : <GuideResult result={item} />
|
|
74
74
|
}
|
|
75
|
-
onSelectListItem={(item) =>
|
|
75
|
+
onSelectListItem={(item) =>
|
|
76
|
+
onSelect?.((item as any)['data']?.['url'] ?? (item as any)['stainlessPath'])
|
|
77
|
+
}
|
|
76
78
|
/>
|
|
77
79
|
</div>
|
|
78
80
|
);
|
|
@@ -81,7 +83,7 @@ export function SearchForm() {
|
|
|
81
83
|
export type SearchFilterProps = {
|
|
82
84
|
results: ResultData;
|
|
83
85
|
filterKind: QueryKindsType;
|
|
84
|
-
onChange: (filterKind
|
|
86
|
+
onChange: (filterKind: QueryKindsType) => void;
|
|
85
87
|
};
|
|
86
88
|
|
|
87
89
|
export function SearchFilter({ results, filterKind, onChange }: SearchFilterProps) {
|
|
@@ -111,7 +113,7 @@ export type SearchModalProps = {
|
|
|
111
113
|
};
|
|
112
114
|
|
|
113
115
|
export function SearchModal({ id, open: isOpen }: SearchModalProps) {
|
|
114
|
-
const [open, setOpen] = React.useState<boolean>(isOpen);
|
|
116
|
+
const [open, setOpen] = React.useState<boolean>(isOpen ?? false);
|
|
115
117
|
|
|
116
118
|
return (
|
|
117
119
|
<div
|
package/src/search/indexer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { generateRoute, Languages, parseStainlessPath, walkTree } from '../routing';
|
|
2
2
|
import * as printer from './printer';
|
|
3
|
-
import type * as SDKJSON from '
|
|
3
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
4
4
|
import type { IndexEntry } from './types';
|
|
5
5
|
import { renderMarkdown } from '../markdown';
|
|
6
6
|
|
|
@@ -93,8 +93,8 @@ export function* generateChatIndex(spec: SDKJSON.Spec) {
|
|
|
93
93
|
summary,
|
|
94
94
|
description,
|
|
95
95
|
stainlessPath,
|
|
96
|
-
qualified: decl['qualified'],
|
|
97
|
-
ident: decl['ident'],
|
|
96
|
+
qualified: 'qualified' in decl ? decl['qualified'] : undefined,
|
|
97
|
+
ident: 'ident' in decl ? decl['ident'] : undefined,
|
|
98
98
|
content: chunk,
|
|
99
99
|
url: generateRoute('docs://BASE_PATH', language, stainlessPath),
|
|
100
100
|
};
|
|
@@ -105,7 +105,7 @@ export function* generateChatIndex(spec: SDKJSON.Spec) {
|
|
|
105
105
|
|
|
106
106
|
export function* generateIndex(
|
|
107
107
|
spec: SDKJSON.Spec,
|
|
108
|
-
renderMarkdownFn?: (string
|
|
108
|
+
renderMarkdownFn?: (_: string) => string | null,
|
|
109
109
|
includeTypes?: boolean,
|
|
110
110
|
): Generator<IndexEntry> {
|
|
111
111
|
const parentCrumbs: Record<string, string[]> = {};
|
|
@@ -113,8 +113,8 @@ export function* generateIndex(
|
|
|
113
113
|
const { kind, name, title, stainlessPath } = data;
|
|
114
114
|
const common = { name, title, stainlessPath };
|
|
115
115
|
|
|
116
|
-
const parsedPath = parseStainlessPath(stainlessPath)
|
|
117
|
-
const crumbs = getResourceNames(parsedPath.resource
|
|
116
|
+
const parsedPath = parseStainlessPath(stainlessPath)!;
|
|
117
|
+
const crumbs = getResourceNames(parsedPath.resource!, spec.resources);
|
|
118
118
|
|
|
119
119
|
switch (kind) {
|
|
120
120
|
case 'resource':
|
|
@@ -144,7 +144,7 @@ export function* generateIndex(
|
|
|
144
144
|
if (!found) continue;
|
|
145
145
|
|
|
146
146
|
parentCrumbs[stainlessPath] = [...crumbs, title];
|
|
147
|
-
const qualified = found['qualified'];
|
|
147
|
+
const qualified = 'qualified' in found ? found['qualified'] : undefined;
|
|
148
148
|
const ident = qualified?.split('.')?.at(-1);
|
|
149
149
|
|
|
150
150
|
yield {
|
|
@@ -153,7 +153,9 @@ export function* generateIndex(
|
|
|
153
153
|
ident,
|
|
154
154
|
qualified,
|
|
155
155
|
language,
|
|
156
|
-
description:
|
|
156
|
+
description: data.description
|
|
157
|
+
? (renderMarkdownFn?.(data.description) ?? data.description)
|
|
158
|
+
: undefined,
|
|
157
159
|
endpoint: endpoint.slice(httpMethod.length).trim(),
|
|
158
160
|
httpMethod,
|
|
159
161
|
summary,
|
|
@@ -172,9 +174,9 @@ export function* generateIndex(
|
|
|
172
174
|
parentCrumbs[stainlessPath] = [...crumbs, title];
|
|
173
175
|
const schema = spec.decls[language]?.[`${stainlessPath} > (schema)`];
|
|
174
176
|
const children =
|
|
175
|
-
schema?.['children']
|
|
177
|
+
(schema && 'children' in schema ? schema?.['children'] : undefined)
|
|
176
178
|
?.map((childPath) => {
|
|
177
|
-
const child = spec.decls?.[language]?.[childPath];
|
|
179
|
+
const child = spec.decls?.[language]?.[childPath] as any;
|
|
178
180
|
return (
|
|
179
181
|
child?.['ident'] ??
|
|
180
182
|
child?.['name'] ??
|
|
@@ -192,7 +194,7 @@ export function* generateIndex(
|
|
|
192
194
|
children,
|
|
193
195
|
language,
|
|
194
196
|
priority: 2,
|
|
195
|
-
ident: schema?.['ident'],
|
|
197
|
+
ident: schema && 'ident' in schema ? schema?.['ident'] : undefined,
|
|
196
198
|
...common,
|
|
197
199
|
};
|
|
198
200
|
}
|
|
@@ -213,11 +215,11 @@ export function* generateIndex(
|
|
|
213
215
|
case 'HttpDeclProperty':
|
|
214
216
|
case 'TSDeclProperty':
|
|
215
217
|
{
|
|
216
|
-
const parsedPath = parseStainlessPath(decl.stainlessPath)
|
|
218
|
+
const parsedPath = parseStainlessPath(decl.stainlessPath)!;
|
|
217
219
|
const type = includeTypes === false ? undefined : printer.typeName(language, decl.type);
|
|
218
|
-
const name = decl['ident'] ?? decl['name'] ?? decl['key'];
|
|
220
|
+
const name: string = (decl as any)['ident'] ?? (decl as any)['name'] ?? (decl as any)['key'];
|
|
219
221
|
|
|
220
|
-
const parent = parentCrumbs[parsedPath.routable];
|
|
222
|
+
const parent = parentCrumbs[parsedPath.routable!];
|
|
221
223
|
// Filter out properties of non-routable response types
|
|
222
224
|
if (parent === undefined) continue;
|
|
223
225
|
|
|
@@ -232,7 +234,7 @@ export function* generateIndex(
|
|
|
232
234
|
name,
|
|
233
235
|
stainlessPath: decl.stainlessPath,
|
|
234
236
|
crumbs: [...parent, ...props],
|
|
235
|
-
docstring: renderMarkdownFn?.(decl.docstring) ?? decl.docstring,
|
|
237
|
+
docstring: decl.docstring ? (renderMarkdownFn?.(decl.docstring) ?? decl.docstring) : undefined,
|
|
236
238
|
type,
|
|
237
239
|
language,
|
|
238
240
|
priority: 3,
|
package/src/search/mcp.ts
CHANGED
|
@@ -1,28 +1,38 @@
|
|
|
1
|
+
import { BM25Retriever } from '@langchain/community/retrievers/bm25';
|
|
1
2
|
import { renderMarkdown } from '../markdown';
|
|
2
3
|
import { DocsLanguage, parseStainlessPath } from '../routing';
|
|
3
4
|
import { getResourceFromSpec } from '../utils';
|
|
4
|
-
import { buildIndex, search } from './providers/fuse';
|
|
5
|
-
import type { IndexEntry } from './types';
|
|
6
|
-
import
|
|
5
|
+
import { buildIndex, search as fuseSearch } from './providers/fuse';
|
|
6
|
+
import type { IndexEntry, IndexMethod } from './types';
|
|
7
|
+
import natural from 'natural';
|
|
8
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
9
|
+
|
|
10
|
+
type Item = IndexEntry & IndexMethod;
|
|
11
|
+
|
|
12
|
+
export interface SearchResult {
|
|
13
|
+
item: Item;
|
|
14
|
+
score?: number;
|
|
15
|
+
refIndex: number;
|
|
16
|
+
}
|
|
7
17
|
|
|
8
18
|
export function consolidate(results: IndexEntry[]) {
|
|
9
19
|
const resources = new Set<string>();
|
|
10
20
|
const methods = new Set<string>();
|
|
11
21
|
|
|
12
22
|
for (const entry of results) {
|
|
13
|
-
const parsed = parseStainlessPath(entry.stainlessPath)
|
|
14
|
-
if (parsed.method) methods.add(parsed.routable);
|
|
15
|
-
else resources.add(parsed.routable);
|
|
23
|
+
const parsed = parseStainlessPath(entry.stainlessPath)!;
|
|
24
|
+
if (parsed.method) methods.add(parsed.routable!);
|
|
25
|
+
else resources.add(parsed.routable!);
|
|
16
26
|
}
|
|
17
27
|
|
|
18
|
-
const filtered = Array.from(methods).filter((path) => !resources.has(path.split(' >').at(0)));
|
|
28
|
+
const filtered = Array.from(methods).filter((path) => !resources.has(path.split(' >').at(0)!));
|
|
19
29
|
return [...resources, ...filtered];
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
export function render(
|
|
23
33
|
spec: SDKJSON.Spec,
|
|
24
34
|
language: DocsLanguage,
|
|
25
|
-
items:
|
|
35
|
+
items: Item[],
|
|
26
36
|
includeModelProperties: boolean,
|
|
27
37
|
) {
|
|
28
38
|
const env = {
|
|
@@ -36,9 +46,9 @@ export function render(
|
|
|
36
46
|
|
|
37
47
|
const paths = consolidate(items);
|
|
38
48
|
const output = paths.map((entry) => {
|
|
39
|
-
const parsed = parseStainlessPath(entry)
|
|
40
|
-
const resource = getResourceFromSpec(parsed.resource
|
|
41
|
-
const target = parsed.method ? resource.methods[parsed.method] : resource;
|
|
49
|
+
const parsed = parseStainlessPath(entry)!;
|
|
50
|
+
const resource = getResourceFromSpec(parsed.resource!, spec)!;
|
|
51
|
+
const target = parsed.method ? resource.methods[parsed.method]! : resource;
|
|
42
52
|
const content = renderMarkdown(env, target);
|
|
43
53
|
return [entry, content];
|
|
44
54
|
});
|
|
@@ -46,16 +56,98 @@ export function render(
|
|
|
46
56
|
return Object.fromEntries(output);
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
export function searchAndRender(
|
|
59
|
+
export async function searchAndRender(
|
|
50
60
|
spec: SDKJSON.Spec,
|
|
51
61
|
language: DocsLanguage,
|
|
52
62
|
query: string,
|
|
53
63
|
limit?: number,
|
|
54
64
|
includeModelProperties: boolean = false,
|
|
55
65
|
) {
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
return render(spec, language,
|
|
66
|
+
const results = await search(spec, language, query, limit);
|
|
67
|
+
const items = results.map(({ item }) => item);
|
|
68
|
+
return render(spec, language, items, includeModelProperties);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function search(
|
|
72
|
+
spec: SDKJSON.Spec,
|
|
73
|
+
language: DocsLanguage,
|
|
74
|
+
query: string,
|
|
75
|
+
limit: number = 100,
|
|
76
|
+
): Promise<SearchResult[]> {
|
|
77
|
+
const fuseIndex = buildIndex(spec, language);
|
|
78
|
+
|
|
79
|
+
// only HTTP methods are useful for MCP
|
|
80
|
+
const httpMethodEntries = fuseIndex.content.filter((entry) => entry.kind === 'http_method') as Item[];
|
|
81
|
+
|
|
82
|
+
// build BM25 retriever if we have HTTP methods
|
|
83
|
+
let bm25Retriever: BM25Retriever | null = null;
|
|
84
|
+
if (httpMethodEntries.length > 0) {
|
|
85
|
+
const documents = httpMethodEntries.map((entry) => {
|
|
86
|
+
// empirically, endpoint + summary seems to be a reasonable semantic encapsulation of what the endpoint does
|
|
87
|
+
const content = `${entry.endpoint} ${entry.summary}`;
|
|
88
|
+
return {
|
|
89
|
+
// stem the content - we will be stemming the query as well
|
|
90
|
+
pageContent: stemText(content),
|
|
91
|
+
metadata: {
|
|
92
|
+
stainlessPath: entry.stainlessPath,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
bm25Retriever = BM25Retriever.fromDocuments(documents, { k: 100 });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// get initial results from Fuse
|
|
101
|
+
const rawResults = fuseSearch(fuseIndex, query, 100).filter(
|
|
102
|
+
(r) => r.item.kind === 'http_method',
|
|
103
|
+
) as SearchResult[];
|
|
104
|
+
|
|
105
|
+
// only keep HTTP methods
|
|
106
|
+
const filtered = rawResults.filter((r) => r.item.kind === 'http_method');
|
|
107
|
+
|
|
108
|
+
// stem the query and apply BM25 reranking
|
|
109
|
+
if (filtered.length > 0 && bm25Retriever) {
|
|
110
|
+
const stemmedQuery = stemText(query);
|
|
111
|
+
|
|
112
|
+
// get BM25-ranked result
|
|
113
|
+
const results = await bm25Retriever.invoke(stemmedQuery);
|
|
114
|
+
const reranked = results.map((doc) => doc.metadata);
|
|
115
|
+
|
|
116
|
+
// build a map of Fuse results by path for fast lookup
|
|
117
|
+
const sortStart = Date.now();
|
|
118
|
+
const fuseResultsByPath = new Map(filtered.map((r) => [r.item.stainlessPath, r]));
|
|
119
|
+
|
|
120
|
+
// use BM25 ordering to reorder Fuse results
|
|
121
|
+
const reorderedResults: SearchResult[] = [];
|
|
122
|
+
for (const doc of reranked) {
|
|
123
|
+
const fuseResult = fuseResultsByPath.get(doc.stainlessPath);
|
|
124
|
+
if (fuseResult) {
|
|
125
|
+
reorderedResults.push(fuseResult);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// replace filtered with reordered results
|
|
130
|
+
filtered.length = 0;
|
|
131
|
+
filtered.push(...reorderedResults);
|
|
132
|
+
console.debug(` [SORT] Reranked in ${Date.now() - sortStart}ms`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return filtered.slice(0, limit);
|
|
59
136
|
}
|
|
60
137
|
|
|
61
|
-
|
|
138
|
+
function stemText(text: string): string {
|
|
139
|
+
if (!text) return '';
|
|
140
|
+
|
|
141
|
+
// tokenize manually and stem each word
|
|
142
|
+
const words = text.toLowerCase().split(/\s+/);
|
|
143
|
+
const stemmedWords = words
|
|
144
|
+
.map((word) => {
|
|
145
|
+
// remove punctuation and stem
|
|
146
|
+
const cleaned = word.replace(/[^\w]/g, '');
|
|
147
|
+
if (!cleaned) return '';
|
|
148
|
+
return natural.LancasterStemmer.stem(cleaned);
|
|
149
|
+
})
|
|
150
|
+
.filter((w) => w);
|
|
151
|
+
|
|
152
|
+
return stemmedWords.join(' ');
|
|
153
|
+
}
|
package/src/search/printer.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
3
|
-
import type * as SDKJSON from '
|
|
3
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
4
4
|
import type { DocsLanguage } from '../routing';
|
|
5
5
|
|
|
6
6
|
import { DocsProvider, useLanguageComponents } from '../contexts';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { searchClient } from '@algolia/client-search';
|
|
2
2
|
import { generateChatIndex, generateIndex } from '../indexer';
|
|
3
3
|
import { SearchableAttributes, SearchableAttributesChat } from '../types';
|
|
4
|
-
import type * as SDKJSON from '
|
|
4
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
5
5
|
import type { ResultRecordType, SearchSettings, SearchParams, ResultType } from '../types';
|
|
6
6
|
|
|
7
7
|
export async function buildIndex(
|
|
@@ -9,7 +9,7 @@ export async function buildIndex(
|
|
|
9
9
|
indexName: string,
|
|
10
10
|
writeKey: string,
|
|
11
11
|
spec: SDKJSON.Spec,
|
|
12
|
-
renderMarkdown: (string
|
|
12
|
+
renderMarkdown: (_: string) => string | null,
|
|
13
13
|
): Promise<void> {
|
|
14
14
|
if (!appId || !indexName || !writeKey) return;
|
|
15
15
|
const objects = Array.from(generateIndex(spec, renderMarkdown));
|
|
@@ -22,7 +22,7 @@ export async function buildIndex(
|
|
|
22
22
|
highlightPostTag: '</mark>',
|
|
23
23
|
customRanking: ['asc(priority)'],
|
|
24
24
|
attributesForFaceting: ['language', 'kind'],
|
|
25
|
-
searchableAttributes: SearchableAttributes,
|
|
25
|
+
searchableAttributes: [...SearchableAttributes],
|
|
26
26
|
},
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -81,8 +81,8 @@ export async function search({
|
|
|
81
81
|
],
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
if ('hits' in results[0] && 'hits' in results[1]) {
|
|
84
|
+
if ('hits' in results[0]! && 'hits' in results[1]!) {
|
|
85
85
|
const [{ nbHits, facets }, { hits }] = results;
|
|
86
|
-
return { hits, nbHits, facets };
|
|
86
|
+
return { hits, nbHits: nbHits ?? 0, facets };
|
|
87
87
|
}
|
|
88
88
|
}
|