@stainless-api/docs 0.1.0-beta.129 → 0.1.0-beta.130
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 +24 -0
- package/ambient.d.ts +6 -0
- package/eslint-suppressions.json +0 -5
- package/package.json +19 -15
- package/plugin/generateAPIReferenceLink.ts +0 -40
- package/plugin/index.ts +12 -0
- package/plugin/loadPluginConfig.ts +36 -5
- package/plugin/markdown/highlighter.ts +1 -1
- package/plugin/react/Routing.tsx +1 -85
- package/plugin/referencePlaceholderUtils.ts +1 -1
- package/plugin/routes/llms.ts +186 -0
- package/plugin/sidebar-utils/sidebar-builder.ts +2 -7
- package/plugin/specs/FileCache.ts +1 -1
- package/plugin/specs/index.ts +1 -6
- package/plugin/vendor/preview.worker.docs.js +9001 -8694
- package/shared/virtualModule.ts +1 -9
- package/stl-docs/chat/docs-chat-handler.ts +18 -0
- package/stl-docs/chat/hook.ts +215 -0
- package/stl-docs/chat/schemas.ts +70 -0
- package/stl-docs/chat/stainless-handler/index.ts +126 -0
- package/stl-docs/chat/stream-util.ts +16 -0
- package/stl-docs/chat/ui/AiChat.module.css +591 -0
- package/stl-docs/chat/ui/AiChat.tsx +188 -0
- package/stl-docs/chat/ui/Trigger.tsx +154 -0
- package/stl-docs/chat/ui/components/ChatControls.tsx +51 -0
- package/stl-docs/chat/ui/components/ChatEmpty.tsx +42 -0
- package/stl-docs/chat/ui/components/ChatLog.tsx +96 -0
- package/stl-docs/chat/ui/components/ChatMessage.tsx +47 -0
- package/stl-docs/chat/ui/components/CodeBlock.tsx +33 -0
- package/stl-docs/chat/ui/components/MessageFeedback.tsx +109 -0
- package/stl-docs/chat/ui/components/Table.tsx +15 -0
- package/stl-docs/chat/ui/components/ToolCall.tsx +34 -0
- package/stl-docs/chat/ui/components/hljs-github.css +81 -0
- package/stl-docs/chat/ui/scroll-manager.ts +86 -0
- package/stl-docs/chat/ui/types.ts +45 -0
- package/stl-docs/components/AiChatIsland.tsx +14 -12
- package/stl-docs/components/PageFrame.astro +7 -4
- package/stl-docs/components/headers/DefaultHeader.astro +2 -2
- package/stl-docs/components/headers/StackedHeader.astro +2 -2
- package/stl-docs/components/mintlify-compat/Accordion.astro +2 -2
- package/stl-docs/components/mintlify-compat/AccordionGroup.astro +0 -4
- package/stl-docs/components/mintlify-compat/Columns.astro +2 -2
- package/stl-docs/components/mintlify-compat/Frame.astro +2 -2
- package/stl-docs/components/mintlify-compat/Tab.astro +2 -2
- package/stl-docs/components/mintlify-compat/callouts/Callout.astro +2 -2
- package/stl-docs/components/mintlify-compat/callouts/Check.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Danger.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Info.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Note.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Tip.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Warning.astro +0 -4
- package/stl-docs/components/nav-tabs/NavDropdown.astro +1 -1
- package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +2 -2
- package/stl-docs/components/pagination/PaginationLinkQuiet.astro +2 -2
- package/stl-docs/components/pagination/util.ts +3 -3
- package/stl-docs/disableCalloutSyntax.ts +1 -1
- package/stl-docs/index.ts +14 -28
- package/stl-docs/loadStlDocsConfig.ts +15 -4
- package/stl-docs/proseSearchIndexing.ts +2 -6
- package/virtual-module.d.ts +8 -17
- package/stl-docs/components/ClientRouterHead.astro +0 -41
package/shared/virtualModule.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ViteUserConfig } from 'astro';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type VitePlugin = NonNullable<ViteUserConfig['plugins']>[number];
|
|
4
4
|
|
|
5
5
|
export function buildVirtualModuleString<T extends Record<string, unknown>>(vars: T) {
|
|
6
6
|
return Object.entries(vars)
|
|
@@ -50,11 +50,3 @@ export function makeAsyncVirtualModPlugin<T extends Record<string, unknown>>(
|
|
|
50
50
|
},
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
export function virtualModule(id: string) {
|
|
55
|
-
return {
|
|
56
|
-
fromString: (content: string) => makeVirtualModPlugin(id, content),
|
|
57
|
-
fromObject: <T extends Record<string, unknown>>(content: T) =>
|
|
58
|
-
makeVirtualModPlugin(id, buildVirtualModuleString(content)),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { FeedbackResponseBody, MetadataResponseBody, RequestBody, ResponseChunk } from './schemas';
|
|
2
|
+
|
|
3
|
+
export type DocsChatHandler = {
|
|
4
|
+
generateResponse: (
|
|
5
|
+
{
|
|
6
|
+
query,
|
|
7
|
+
priorMessages,
|
|
8
|
+
}: {
|
|
9
|
+
query: string;
|
|
10
|
+
priorMessages: NonNullable<RequestBody['additionalContext']>['prior_messages'];
|
|
11
|
+
},
|
|
12
|
+
abortSignal: AbortSignal,
|
|
13
|
+
) => AsyncGenerator<ResponseChunk>;
|
|
14
|
+
|
|
15
|
+
onRate: (spanId: string, score: 0 | 1) => Promise<FeedbackResponseBody>;
|
|
16
|
+
|
|
17
|
+
onAssignMetadata: (spanId: string, metadata: Record<string, string>) => Promise<MetadataResponseBody>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { AssistantTextMessage, AssistantToolCallMessage, ChatMessage, UserMessage } from './ui/types';
|
|
2
|
+
import { useCallback, useEffect, useReducer, useRef } from 'react';
|
|
3
|
+
import type { ResponseChunk } from './schemas';
|
|
4
|
+
import { DocsChatHandler } from './docs-chat-handler';
|
|
5
|
+
|
|
6
|
+
//
|
|
7
|
+
// Reducer
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Splice a new assistant message into a ChatMessage[] stream based on its respondingTo property.
|
|
12
|
+
*
|
|
13
|
+
* if two responses are streaming at once, we want to put all the responses to user Message A
|
|
14
|
+
* before user message B
|
|
15
|
+
*/
|
|
16
|
+
function spliceNewMessage(messages: ChatMessage[], newMessage: Extract<ChatMessage, { role: 'assistant' }>) {
|
|
17
|
+
// find the most recent assistant message that's responding to the same user message as we are
|
|
18
|
+
let insertAfterIdx = messages.findLastIndex(
|
|
19
|
+
(msg) =>
|
|
20
|
+
(msg.role === 'assistant' && msg.respondingTo === newMessage.respondingTo) ||
|
|
21
|
+
// if this is the first assistant message responding to this user message
|
|
22
|
+
(msg.role === 'user' && msg.id === newMessage.respondingTo),
|
|
23
|
+
);
|
|
24
|
+
insertAfterIdx = insertAfterIdx === -1 ? messages.length - 1 : insertAfterIdx;
|
|
25
|
+
return messages.toSpliced(insertAfterIdx + 1, 0, newMessage);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type ChatReducerAction =
|
|
29
|
+
| { type: 'addUserMessage'; content: UserMessage['content']; id: string }
|
|
30
|
+
| { type: 'beginAssistantMessage'; message: Omit<AssistantTextMessage, 'role' | 'isComplete'> }
|
|
31
|
+
| { type: 'streamMessage'; id: string; newContent: string }
|
|
32
|
+
| { type: 'completeMessage'; id: string }
|
|
33
|
+
| { type: 'addAssistantToolCall'; message: Omit<AssistantToolCallMessage, 'role' | 'id'> }
|
|
34
|
+
// a response potentially contains multiple messages / tool calls
|
|
35
|
+
| { type: 'completeResponse'; respondingTo: UserMessage['id']; spanId: string }
|
|
36
|
+
| { type: 'addError'; respondingTo: UserMessage['id']; errorMessage: string };
|
|
37
|
+
|
|
38
|
+
function chatReducer(state: ChatMessage[], action: ChatReducerAction) {
|
|
39
|
+
if (action.type === 'addUserMessage') {
|
|
40
|
+
return [...state, { role: 'user', content: action.content, id: action.id } satisfies ChatMessage];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (action.type === 'beginAssistantMessage') {
|
|
44
|
+
return spliceNewMessage(state, {
|
|
45
|
+
role: 'assistant',
|
|
46
|
+
id: action.message.id,
|
|
47
|
+
respondingTo: action.message.respondingTo,
|
|
48
|
+
messageType: action.message.messageType satisfies 'text',
|
|
49
|
+
content: action.message.content,
|
|
50
|
+
isComplete: false,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (action.type === 'streamMessage') {
|
|
55
|
+
return state.map((msg) =>
|
|
56
|
+
msg.id === action.id && 'content' in msg
|
|
57
|
+
? ({ ...msg, content: `${msg.content}${action.newContent}` } satisfies ChatMessage)
|
|
58
|
+
: msg,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (action.type === 'completeMessage') {
|
|
63
|
+
return state.map((msg) =>
|
|
64
|
+
msg.id === action.id && msg.role === 'assistant' && msg.messageType === 'text'
|
|
65
|
+
? ({ ...msg, isComplete: true } satisfies ChatMessage)
|
|
66
|
+
: msg,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (action.type === 'addAssistantToolCall') {
|
|
71
|
+
return spliceNewMessage(state, {
|
|
72
|
+
role: 'assistant',
|
|
73
|
+
id: crypto.randomUUID(),
|
|
74
|
+
messageType: action.message.messageType satisfies 'tool_use',
|
|
75
|
+
respondingTo: action.message.respondingTo,
|
|
76
|
+
toolName: action.message.toolName,
|
|
77
|
+
input: action.message.input,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (action.type === 'completeResponse') {
|
|
82
|
+
return spliceNewMessage(state, {
|
|
83
|
+
role: 'assistant',
|
|
84
|
+
id: crypto.randomUUID(),
|
|
85
|
+
messageType: 'done',
|
|
86
|
+
respondingTo: action.respondingTo,
|
|
87
|
+
spanId: action.spanId,
|
|
88
|
+
} satisfies Extract<ChatMessage, { role: 'assistant' }>);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (action.type === 'addError') {
|
|
92
|
+
return spliceNewMessage(state, {
|
|
93
|
+
role: 'assistant',
|
|
94
|
+
id: crypto.randomUUID(),
|
|
95
|
+
messageType: 'error',
|
|
96
|
+
respondingTo: action.respondingTo,
|
|
97
|
+
errorMessage: action.errorMessage,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return state;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//
|
|
105
|
+
// Consumable hook
|
|
106
|
+
//
|
|
107
|
+
|
|
108
|
+
export function useChat({ handler }: { handler: DocsChatHandler }) {
|
|
109
|
+
// Used to clean up stray streaming requests on unmount (prevent setState on unmounted component)
|
|
110
|
+
const abortControllerRef = useRef(new AbortController());
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
abortControllerRef.current = abortControllerRef.current.signal.aborted
|
|
113
|
+
? new AbortController()
|
|
114
|
+
: abortControllerRef.current;
|
|
115
|
+
const ac = abortControllerRef.current;
|
|
116
|
+
return () => ac.abort('Component unmounted');
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
const [chatMessages, dispatch] = useReducer(chatReducer, []);
|
|
120
|
+
|
|
121
|
+
/** Send a message and stream back the response in chat */
|
|
122
|
+
const sendMessage = useCallback(
|
|
123
|
+
async (question: string) => {
|
|
124
|
+
const userMessageId = crypto.randomUUID();
|
|
125
|
+
dispatch({ type: 'addUserMessage', content: question, id: userMessageId });
|
|
126
|
+
|
|
127
|
+
let currentResponseId = crypto.randomUUID(); // for streaming text messages
|
|
128
|
+
let lastChunkType: ResponseChunk['type'] | undefined = undefined;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
let chunk: ResponseChunk | undefined = undefined;
|
|
132
|
+
for await (chunk of handler.generateResponse(
|
|
133
|
+
{
|
|
134
|
+
query: question,
|
|
135
|
+
priorMessages: chatMessages.filter(
|
|
136
|
+
(msg) => msg.role === 'user' || (msg.role === 'assistant' && msg.messageType === 'text'),
|
|
137
|
+
),
|
|
138
|
+
},
|
|
139
|
+
abortControllerRef.current.signal,
|
|
140
|
+
)) {
|
|
141
|
+
if (abortControllerRef.current.signal.aborted) break;
|
|
142
|
+
|
|
143
|
+
// mark complete when text messages finish streaming
|
|
144
|
+
if (lastChunkType === 'text' && chunk.type !== 'text') {
|
|
145
|
+
dispatch({ type: 'completeMessage', id: currentResponseId });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (chunk.type === 'done') {
|
|
149
|
+
dispatch({ type: 'completeResponse', respondingTo: userMessageId, spanId: chunk.span_id });
|
|
150
|
+
// stop reading from the stream on done
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (chunk.type === 'text') {
|
|
155
|
+
if (lastChunkType !== 'text') {
|
|
156
|
+
// start a new text message
|
|
157
|
+
currentResponseId = crypto.randomUUID();
|
|
158
|
+
dispatch({
|
|
159
|
+
type: 'beginAssistantMessage',
|
|
160
|
+
message: {
|
|
161
|
+
content: chunk.text,
|
|
162
|
+
id: currentResponseId,
|
|
163
|
+
messageType: chunk.type,
|
|
164
|
+
respondingTo: userMessageId,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
// continue the current message with the new content
|
|
169
|
+
dispatch({ type: 'streamMessage', id: currentResponseId, newContent: chunk.text });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (chunk.type === 'tool_use') {
|
|
174
|
+
dispatch({
|
|
175
|
+
type: 'addAssistantToolCall',
|
|
176
|
+
message: {
|
|
177
|
+
respondingTo: userMessageId,
|
|
178
|
+
messageType: chunk.type,
|
|
179
|
+
toolName: chunk.name,
|
|
180
|
+
input: chunk.input,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
lastChunkType = chunk.type;
|
|
186
|
+
}
|
|
187
|
+
if (!chunk || lastChunkType === 'start_session') {
|
|
188
|
+
dispatch({
|
|
189
|
+
type: 'addError',
|
|
190
|
+
respondingTo: userMessageId,
|
|
191
|
+
errorMessage: 'No response received. Please try again.',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
dispatch({
|
|
196
|
+
type: 'addError',
|
|
197
|
+
respondingTo: userMessageId,
|
|
198
|
+
errorMessage: 'Something went wrong. Please try again.',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
[chatMessages, handler],
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
async function rateMessage(spanId: string, rating: 'up' | 'down'): Promise<boolean> {
|
|
206
|
+
try {
|
|
207
|
+
const { success } = await handler.onRate(spanId, { up: 1 as const, down: 0 as const }[rating]);
|
|
208
|
+
return success;
|
|
209
|
+
} catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { chatMessages, sendMessage, rateMessage, setMetadata: handler.onAssignMetadata.bind(handler) };
|
|
215
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
|
|
3
|
+
// TODO: replace with generated SDK types instead of copy/pasting from other repo
|
|
4
|
+
export const requestBody = z.object({
|
|
5
|
+
query: z.string(),
|
|
6
|
+
sdk: z.object({
|
|
7
|
+
project: z.string(),
|
|
8
|
+
language: z.string(),
|
|
9
|
+
version: z.string().optional(),
|
|
10
|
+
}),
|
|
11
|
+
stream: z.boolean().optional(),
|
|
12
|
+
budget: z
|
|
13
|
+
.object({
|
|
14
|
+
maxTokens: z.number().optional(),
|
|
15
|
+
})
|
|
16
|
+
.optional(),
|
|
17
|
+
session_id: z.string().optional(),
|
|
18
|
+
additionalContext: z
|
|
19
|
+
.object({
|
|
20
|
+
prior_messages: z.array(
|
|
21
|
+
z.object({
|
|
22
|
+
role: z.enum(['user', 'assistant']),
|
|
23
|
+
content: z.string(),
|
|
24
|
+
}),
|
|
25
|
+
),
|
|
26
|
+
code: z.string().optional(),
|
|
27
|
+
intent: z.string().optional(),
|
|
28
|
+
lsp: z.string().optional(),
|
|
29
|
+
errors: z.string().optional(),
|
|
30
|
+
})
|
|
31
|
+
.optional(),
|
|
32
|
+
browser_id: z.string().optional(),
|
|
33
|
+
});
|
|
34
|
+
export type RequestBody = z.input<typeof requestBody>;
|
|
35
|
+
|
|
36
|
+
export const responseChunk = z.discriminatedUnion('type', [
|
|
37
|
+
z.object({
|
|
38
|
+
type: z.literal('text'),
|
|
39
|
+
text: z.string(),
|
|
40
|
+
}),
|
|
41
|
+
z.object({
|
|
42
|
+
type: z.literal('tool_use'),
|
|
43
|
+
name: z.string(),
|
|
44
|
+
input: z.record(z.string(), z.unknown()).optional(),
|
|
45
|
+
}),
|
|
46
|
+
z.object({
|
|
47
|
+
type: z.literal('tool_result'),
|
|
48
|
+
tool_use_id: z.string(),
|
|
49
|
+
content: z.string(),
|
|
50
|
+
}),
|
|
51
|
+
z.object({
|
|
52
|
+
type: z.literal('done'),
|
|
53
|
+
span_id: z.string(),
|
|
54
|
+
}),
|
|
55
|
+
z.object({
|
|
56
|
+
type: z.literal('start_session'),
|
|
57
|
+
session_id: z.string(),
|
|
58
|
+
}),
|
|
59
|
+
]);
|
|
60
|
+
export type ResponseChunk = z.infer<typeof responseChunk>;
|
|
61
|
+
|
|
62
|
+
export const feedbackRequestBody = z.object({ score: z.number().min(0).max(1) });
|
|
63
|
+
export type FeedbackRequestBody = z.infer<typeof feedbackRequestBody>;
|
|
64
|
+
export const feedbackResponseBody = z.object({ success: z.boolean() });
|
|
65
|
+
export type FeedbackResponseBody = z.infer<typeof feedbackResponseBody>;
|
|
66
|
+
|
|
67
|
+
export const metadataRequestBody = z.object({ metadata: z.record(z.string(), z.string()) });
|
|
68
|
+
export type MetadataRequestBody = z.infer<typeof metadataRequestBody>;
|
|
69
|
+
export const metadataResponseBody = z.object({ success: z.boolean() });
|
|
70
|
+
export type MetadataResponseBody = z.infer<typeof metadataResponseBody>;
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
}
|