Skip to content

Commit d99630a

Browse files
committed
Fix thread and display tool calls
1 parent 85f9e22 commit d99630a

File tree

6 files changed

+208
-61
lines changed

6 files changed

+208
-61
lines changed

packages/gitbook/src/components/AI/server-actions/AIMessageView.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { AIMessage } from '@gitbook/api';
2+
import type { GitBookSiteContext } from '@v2/lib/context';
23
import { DocumentView } from '../../DocumentView';
4+
import { AIToolCallsSummary } from './AIToolCallsSummary';
35
import type { RenderAIMessageOptions } from './types';
46

57
/**
@@ -8,9 +10,10 @@ import type { RenderAIMessageOptions } from './types';
810
export function AIMessageView(
911
props: RenderAIMessageOptions & {
1012
message: AIMessage;
13+
context: GitBookSiteContext;
1114
}
1215
) {
13-
const { message } = props;
16+
const { message, context } = props;
1417

1518
return (
1619
<div className="flex flex-col gap-2">
@@ -26,6 +29,9 @@ export function AIMessageView(
2629
}}
2730
style={['space-y-5']}
2831
/>
32+
{step.toolCalls && step.toolCalls.length > 0 ? (
33+
<AIToolCallsSummary toolCalls={step.toolCalls} context={context} />
34+
) : null}
2935
</div>
3036
);
3137
})}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { Link } from '@/components/primitives';
2+
import { resolveContentRef } from '@/lib/references';
3+
import type { AIToolCall, ContentRef } from '@gitbook/api';
4+
import { Icon, type IconName } from '@gitbook/icons';
5+
import type { GitBookSiteContext } from '@v2/lib/context';
6+
import type * as React from 'react';
7+
8+
/**
9+
* Display the tool calls in a message or step.
10+
*/
11+
export function AIToolCallsSummary(props: {
12+
toolCalls: AIToolCall[];
13+
context: GitBookSiteContext;
14+
}) {
15+
const { toolCalls, context } = props;
16+
17+
return (
18+
<div className="flex flex-col gap-1">
19+
{toolCalls.map((toolCall, index) => (
20+
<ToolCallSummary key={index} toolCall={toolCall} context={context} />
21+
))}
22+
</div>
23+
);
24+
}
25+
26+
function ToolCallSummary(props: {
27+
toolCall: AIToolCall;
28+
context: GitBookSiteContext;
29+
}) {
30+
const { toolCall, context } = props;
31+
32+
return (
33+
<p className="text-slate-700 text-sm">
34+
<Icon
35+
icon={getIconForToolCall(toolCall)}
36+
className="mr-1 inline-block size-3 text-slate-300"
37+
/>
38+
{getDescriptionForToolCall(toolCall, context)}
39+
</p>
40+
);
41+
}
42+
43+
function getDescriptionForToolCall(
44+
toolCall: AIToolCall,
45+
context: GitBookSiteContext
46+
): React.ReactNode {
47+
switch (toolCall.tool) {
48+
case 'getPageContent':
49+
return (
50+
<>
51+
Read page{' '}
52+
<ContentRefLink
53+
contentRef={{
54+
kind: 'page',
55+
page: toolCall.page.id,
56+
space: toolCall.spaceId,
57+
}}
58+
context={context}
59+
fallback={toolCall.page.title}
60+
/>
61+
<OtherSpaceLink spaceId={toolCall.spaceId} context={context} />
62+
</>
63+
);
64+
case 'search':
65+
// TODO: Show in a popover the results using the list `toolCall.results`.
66+
return (
67+
<>
68+
Searched <strong>{toolCall.query}</strong>
69+
</>
70+
);
71+
case 'getPages':
72+
return (
73+
<>
74+
Listed the pages
75+
<OtherSpaceLink spaceId={toolCall.spaceId} context={context} />
76+
</>
77+
);
78+
default:
79+
return <>{toolCall.tool}</>;
80+
}
81+
}
82+
83+
function getIconForToolCall(toolCall: AIToolCall): IconName {
84+
switch (toolCall.tool) {
85+
case 'getPageContent':
86+
return 'memo';
87+
case 'search':
88+
return 'magnifying-glass';
89+
case 'getPages':
90+
return 'files';
91+
default:
92+
return 'hammer';
93+
}
94+
}
95+
96+
/**
97+
* Link to a space that is not the current space.
98+
*/
99+
function OtherSpaceLink(props: {
100+
spaceId: string;
101+
context: GitBookSiteContext;
102+
prefix?: React.ReactNode;
103+
}) {
104+
const { spaceId, prefix = ' in ', context } = props;
105+
106+
if (context.space.id === spaceId) {
107+
return null;
108+
}
109+
110+
return (
111+
<>
112+
{prefix}
113+
<ContentRefLink
114+
contentRef={{
115+
kind: 'space',
116+
space: spaceId,
117+
}}
118+
context={context}
119+
/>
120+
</>
121+
);
122+
}
123+
124+
async function ContentRefLink(props: {
125+
contentRef: ContentRef;
126+
context: GitBookSiteContext;
127+
fallback?: React.ReactNode;
128+
}) {
129+
const { contentRef, context, fallback } = props;
130+
131+
const resolved = await resolveContentRef(contentRef, context);
132+
133+
if (!resolved) {
134+
return <span>{fallback}</span>;
135+
}
136+
137+
return (
138+
<Link href={resolved.href} className="text-inherit underline decoration-dashed">
139+
{resolved.text}
140+
</Link>
141+
);
142+
}

packages/gitbook/src/components/AI/server-actions/api.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
type AIStreamResponse,
99
} from '@gitbook/api';
1010
import type { GitBookBaseContext } from '@v2/lib/context';
11+
import { fetchServerActionSiteContext } from '@v2/lib/server-actions';
1112
import { EventIterator } from 'event-iterator';
1213
import type { MaybePromise } from 'p-map';
1314
import * as partialJson from 'partial-json';
@@ -85,6 +86,7 @@ export async function streamGenerateAIObject<T>(
8586
* Stream the generation of a document.
8687
*/
8788
export async function streamRenderAIMessage(
89+
baseContext: GitBookBaseContext,
8890
rawStream: AsyncIterable<AIStreamResponse>,
8991
options?: RenderAIMessageOptions
9092
) {
@@ -123,10 +125,13 @@ export async function streamRenderAIMessage(
123125
}
124126
};
125127

128+
// Fetch the full-context in the background to avoid blocking the stream.
129+
const promiseContext = fetchServerActionSiteContext(baseContext);
130+
126131
return parseResponse<{
127132
content: React.ReactNode;
128133
event: AIStreamResponse;
129-
}>(rawStream, (event) => {
134+
}>(rawStream, async (event) => {
130135
switch (event.type) {
131136
/**
132137
* The agent is processing a tool call in a new message.
@@ -172,7 +177,9 @@ export async function streamRenderAIMessage(
172177

173178
return {
174179
event,
175-
content: <AIMessageView message={message} {...options} />,
180+
content: (
181+
<AIMessageView message={message} context={await promiseContext} {...options} />
182+
),
176183
};
177184
});
178185
}
@@ -182,7 +189,7 @@ export async function streamRenderAIMessage(
182189
*/
183190
function parseResponse<T>(
184191
responseStream: EventIterator<AIStreamResponse>,
185-
parse: (response: AIStreamResponse) => T | undefined
192+
parse: (response: AIStreamResponse) => T | undefined | Promise<T | undefined>
186193
): {
187194
stream: EventIterator<T>;
188195
response: Promise<{ responseId: string }>;
@@ -197,14 +204,14 @@ function parseResponse<T>(
197204
let foundResponse = false;
198205

199206
for await (const event of responseStream) {
207+
const parsed = await parse(event);
208+
if (parsed !== undefined) {
209+
queue.push(parsed);
210+
}
211+
200212
if (event.type === 'response_finish') {
201213
foundResponse = true;
202214
resolveResponse({ responseId: event.responseId });
203-
} else {
204-
const parsed = parse(event);
205-
if (parsed !== undefined) {
206-
queue.push(parsed);
207-
}
208215
}
209216
}
210217

packages/gitbook/src/components/AI/server-actions/chat.ts

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,33 +28,29 @@ export async function* streamAIChatResponse({
2828
previousResponseId?: string;
2929
options?: RenderAIMessageOptions;
3030
}) {
31-
const { dataFetcher } = await getServerActionBaseContext();
31+
const context = await getServerActionBaseContext();
3232
const siteURLData = await getSiteURLDataFromMiddleware();
3333

34-
const api = await dataFetcher.api();
35-
const rawStream = await api.orgs.streamAiResponseInSite(
36-
siteURLData.organization,
37-
siteURLData.site,
38-
{
39-
input: [
40-
{
41-
role: AIMessageRole.User,
42-
content: message,
43-
},
44-
],
45-
output: { type: 'document' },
46-
model: AIModel.ReasoningLow,
47-
instructions: PROMPT,
48-
previousResponseId,
49-
tools: {
50-
getPageContent: true,
51-
getPages: true,
52-
search: true,
34+
const api = await context.dataFetcher.api();
35+
const rawStream = api.orgs.streamAiResponseInSite(siteURLData.organization, siteURLData.site, {
36+
input: [
37+
{
38+
role: AIMessageRole.User,
39+
content: message,
5340
},
54-
}
55-
);
56-
57-
const { stream } = await streamRenderAIMessage(rawStream, options);
41+
],
42+
output: { type: 'document' },
43+
model: AIModel.ReasoningLow,
44+
instructions: PROMPT,
45+
previousResponseId,
46+
tools: {
47+
getPageContent: true,
48+
getPages: true,
49+
search: true,
50+
},
51+
});
52+
53+
const { stream } = await streamRenderAIMessage(context, rawStream, options);
5854

5955
for await (const output of stream) {
6056
yield output;

packages/gitbook/src/components/AI/server-actions/pages.ts

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,33 +32,29 @@ export async function* streamGenerateAIPage({
3232
previousResponseId?: string;
3333
options?: RenderAIMessageOptions;
3434
}) {
35-
const { dataFetcher } = await getServerActionBaseContext();
35+
const context = await getServerActionBaseContext();
3636
const siteURLData = await getSiteURLDataFromMiddleware();
3737

38-
const api = await dataFetcher.api();
39-
const rawStream = await api.orgs.streamAiResponseInSite(
40-
siteURLData.organization,
41-
siteURLData.site,
42-
{
43-
input: [
44-
{
45-
role: AIMessageRole.User,
46-
content: query,
47-
},
48-
],
49-
output: { type: 'document' },
50-
model: AIModel.ReasoningLow,
51-
instructions: PROMPT,
52-
previousResponseId,
53-
tools: {
54-
getPageContent: true,
55-
getPages: true,
56-
search: true,
38+
const api = await context.dataFetcher.api();
39+
const rawStream = api.orgs.streamAiResponseInSite(siteURLData.organization, siteURLData.site, {
40+
input: [
41+
{
42+
role: AIMessageRole.User,
43+
content: query,
5744
},
58-
}
59-
);
60-
61-
const { stream } = await streamRenderAIMessage(rawStream, options);
45+
],
46+
output: { type: 'document' },
47+
model: AIModel.ReasoningLow,
48+
instructions: PROMPT,
49+
previousResponseId,
50+
tools: {
51+
getPageContent: true,
52+
getPages: true,
53+
search: true,
54+
},
55+
});
56+
57+
const { stream } = await streamRenderAIMessage(context, rawStream, options);
6258

6359
for await (const output of stream) {
6460
yield output;

packages/gitbook/src/components/AI/server-actions/responses.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@ export async function* streamAIResponseById({
1414
responseId: string;
1515
options?: RenderAIMessageOptions;
1616
}) {
17-
const { dataFetcher } = await getServerActionBaseContext();
17+
const context = await getServerActionBaseContext();
1818
const siteURLData = await getSiteURLDataFromMiddleware();
1919

20-
const api = await dataFetcher.api();
21-
const rawStream = await api.orgs.streamExistingAiResponseInSite(
20+
const api = await context.dataFetcher.api();
21+
const rawStream = api.orgs.streamExistingAiResponseInSite(
2222
siteURLData.organization,
2323
siteURLData.site,
2424
responseId
2525
);
26-
const { stream } = await streamRenderAIMessage(rawStream, options);
26+
const { stream } = await streamRenderAIMessage(context, rawStream, options);
2727

2828
for await (const output of stream) {
2929
yield output;

0 commit comments

Comments
 (0)