@apollo/client-ai-apps 0.2.4 → 0.3.0
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/.git-blame-ignore-revs +2 -0
- package/.github/workflows/pr.yaml +23 -15
- package/.github/workflows/release.yaml +46 -46
- package/.prettierrc +9 -0
- package/dist/apollo_client/client.d.ts +1 -2
- package/dist/apollo_client/link/ToolCallLink.d.ts +26 -0
- package/dist/hooks/useToolEffect.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +75 -25
- package/dist/vite/index.js +74 -21
- package/package.json +5 -2
- package/scripts/dev.mjs +3 -1
- package/src/apollo_client/client.test.ts +226 -13
- package/src/apollo_client/client.ts +44 -30
- package/src/apollo_client/link/ToolCallLink.ts +49 -0
- package/src/apollo_client/provider.tsx +8 -2
- package/src/hooks/useCallTool.ts +8 -3
- package/src/hooks/useOpenAiGlobal.test.ts +3 -1
- package/src/hooks/useOpenAiGlobal.ts +8 -2
- package/src/hooks/useSendFollowUpMessage.test.ts +3 -1
- package/src/hooks/useToolEffect.test.tsx +25 -9
- package/src/hooks/useToolEffect.tsx +24 -5
- package/src/index.ts +1 -0
- package/src/types/openai.ts +5 -2
- package/src/vite/absolute_asset_imports_plugin.test.ts +3 -1
- package/src/vite/absolute_asset_imports_plugin.ts +3 -1
- package/src/vite/application_manifest_plugin.test.ts +169 -46
- package/src/vite/application_manifest_plugin.ts +84 -24
|
@@ -2,6 +2,9 @@ import { expect, test, describe, vi } from "vitest";
|
|
|
2
2
|
import { ExtendedApolloClient } from "./client";
|
|
3
3
|
import { ApplicationManifest } from "../types/application-manifest";
|
|
4
4
|
import { parse } from "graphql";
|
|
5
|
+
import { ApolloLink, HttpLink, InMemoryCache } from "@apollo/client";
|
|
6
|
+
import { ApolloClient } from "..";
|
|
7
|
+
import { ToolCallLink } from "./link/ToolCallLink";
|
|
5
8
|
|
|
6
9
|
describe("Client Basics", () => {
|
|
7
10
|
test("Should execute tool call when client.query is called", async () => {
|
|
@@ -34,7 +37,8 @@ describe("Client Basics", () => {
|
|
|
34
37
|
format: "apollo-ai-app-manifest",
|
|
35
38
|
version: "1",
|
|
36
39
|
name: "the-store",
|
|
37
|
-
description:
|
|
40
|
+
description:
|
|
41
|
+
"An online store selling a variety of high quality products across many different categories.",
|
|
38
42
|
hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
|
|
39
43
|
operations: [
|
|
40
44
|
{
|
|
@@ -44,20 +48,32 @@ describe("Client Basics", () => {
|
|
|
44
48
|
body: "query Product($id: ID!) {\n product(id: $id) {\n id\n title\n rating\n price\n description\n images\n __typename\n }\n}",
|
|
45
49
|
variables: { id: "ID" },
|
|
46
50
|
prefetch: false,
|
|
47
|
-
tools: [
|
|
51
|
+
tools: [
|
|
52
|
+
{
|
|
53
|
+
name: "Get Product",
|
|
54
|
+
description: "Shows the details page for a specific product.",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
48
57
|
},
|
|
49
58
|
],
|
|
50
59
|
resource: "index.html",
|
|
51
60
|
};
|
|
52
61
|
|
|
53
62
|
const client = new ExtendedApolloClient({
|
|
63
|
+
cache: new InMemoryCache(),
|
|
54
64
|
manifest: manifest as ApplicationManifest,
|
|
55
65
|
});
|
|
56
66
|
|
|
57
67
|
const variables = { id: "1" };
|
|
58
|
-
await client.query({
|
|
68
|
+
await client.query({
|
|
69
|
+
query: parse(manifest.operations[0].body),
|
|
70
|
+
variables,
|
|
71
|
+
});
|
|
59
72
|
|
|
60
|
-
expect(window.openai.callTool).toBeCalledWith("execute", {
|
|
73
|
+
expect(window.openai.callTool).toBeCalledWith("execute", {
|
|
74
|
+
query: manifest.operations[0].body,
|
|
75
|
+
variables,
|
|
76
|
+
});
|
|
61
77
|
expect(client.extract()).toMatchInlineSnapshot(`
|
|
62
78
|
{
|
|
63
79
|
"Product:1": {
|
|
@@ -110,7 +126,8 @@ describe("prefetchData", () => {
|
|
|
110
126
|
format: "apollo-ai-app-manifest",
|
|
111
127
|
version: "1",
|
|
112
128
|
name: "the-store",
|
|
113
|
-
description:
|
|
129
|
+
description:
|
|
130
|
+
"An online store selling a variety of high quality products across many different categories.",
|
|
114
131
|
hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
|
|
115
132
|
operations: [
|
|
116
133
|
{
|
|
@@ -120,13 +137,19 @@ describe("prefetchData", () => {
|
|
|
120
137
|
body: "query Product($id: ID!) {\n product(id: $id) {\n id\n title\n rating\n price\n description\n images\n __typename\n }\n}",
|
|
121
138
|
variables: { id: "ID" },
|
|
122
139
|
prefetch: false,
|
|
123
|
-
tools: [
|
|
140
|
+
tools: [
|
|
141
|
+
{
|
|
142
|
+
name: "Get Product",
|
|
143
|
+
description: "Shows the details page for a specific product.",
|
|
144
|
+
},
|
|
145
|
+
],
|
|
124
146
|
},
|
|
125
147
|
],
|
|
126
148
|
resource: "index.html",
|
|
127
149
|
};
|
|
128
150
|
|
|
129
151
|
const client = new ExtendedApolloClient({
|
|
152
|
+
cache: new InMemoryCache(),
|
|
130
153
|
manifest: manifest as ApplicationManifest,
|
|
131
154
|
});
|
|
132
155
|
await client.prefetchData();
|
|
@@ -184,7 +207,8 @@ describe("prefetchData", () => {
|
|
|
184
207
|
format: "apollo-ai-app-manifest",
|
|
185
208
|
version: "1",
|
|
186
209
|
name: "the-store",
|
|
187
|
-
description:
|
|
210
|
+
description:
|
|
211
|
+
"An online store selling a variety of high quality products across many different categories.",
|
|
188
212
|
hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
|
|
189
213
|
operations: [
|
|
190
214
|
{
|
|
@@ -195,13 +219,19 @@ describe("prefetchData", () => {
|
|
|
195
219
|
variables: {},
|
|
196
220
|
prefetch: true,
|
|
197
221
|
prefetchID: "__anonymous",
|
|
198
|
-
tools: [
|
|
222
|
+
tools: [
|
|
223
|
+
{
|
|
224
|
+
name: "Top Products",
|
|
225
|
+
description: "Shows the currently highest rated products.",
|
|
226
|
+
},
|
|
227
|
+
],
|
|
199
228
|
},
|
|
200
229
|
],
|
|
201
230
|
resource: "index.html",
|
|
202
231
|
};
|
|
203
232
|
|
|
204
233
|
const client = new ExtendedApolloClient({
|
|
234
|
+
cache: new InMemoryCache(),
|
|
205
235
|
manifest: manifest as ApplicationManifest,
|
|
206
236
|
});
|
|
207
237
|
await client.prefetchData();
|
|
@@ -273,7 +303,8 @@ describe("prefetchData", () => {
|
|
|
273
303
|
format: "apollo-ai-app-manifest",
|
|
274
304
|
version: "1",
|
|
275
305
|
name: "the-store",
|
|
276
|
-
description:
|
|
306
|
+
description:
|
|
307
|
+
"An online store selling a variety of high quality products across many different categories.",
|
|
277
308
|
hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
|
|
278
309
|
operations: [
|
|
279
310
|
{
|
|
@@ -283,7 +314,12 @@ describe("prefetchData", () => {
|
|
|
283
314
|
body: "query Product($id: ID!) {\n product(id: $id) {\n id\n title\n rating\n price\n description\n images\n __typename\n }\n}",
|
|
284
315
|
variables: { id: "ID" },
|
|
285
316
|
prefetch: false,
|
|
286
|
-
tools: [
|
|
317
|
+
tools: [
|
|
318
|
+
{
|
|
319
|
+
name: "Get Product",
|
|
320
|
+
description: "Shows the details page for a specific product.",
|
|
321
|
+
},
|
|
322
|
+
],
|
|
287
323
|
},
|
|
288
324
|
{
|
|
289
325
|
id: "cd0d52159b9003e791de97c6a76efa03d34fe00cee278d1a3f4bfcec5fb3e1e6",
|
|
@@ -293,13 +329,19 @@ describe("prefetchData", () => {
|
|
|
293
329
|
variables: {},
|
|
294
330
|
prefetch: true,
|
|
295
331
|
prefetchID: "__anonymous",
|
|
296
|
-
tools: [
|
|
332
|
+
tools: [
|
|
333
|
+
{
|
|
334
|
+
name: "Top Products",
|
|
335
|
+
description: "Shows the currently highest rated products.",
|
|
336
|
+
},
|
|
337
|
+
],
|
|
297
338
|
},
|
|
298
339
|
],
|
|
299
340
|
resource: "index.html",
|
|
300
341
|
};
|
|
301
342
|
|
|
302
343
|
const client = new ExtendedApolloClient({
|
|
344
|
+
cache: new InMemoryCache(),
|
|
303
345
|
manifest: manifest as ApplicationManifest,
|
|
304
346
|
});
|
|
305
347
|
await client.prefetchData();
|
|
@@ -367,7 +409,8 @@ describe("prefetchData", () => {
|
|
|
367
409
|
format: "apollo-ai-app-manifest",
|
|
368
410
|
version: "1",
|
|
369
411
|
name: "the-store",
|
|
370
|
-
description:
|
|
412
|
+
description:
|
|
413
|
+
"An online store selling a variety of high quality products across many different categories.",
|
|
371
414
|
hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
|
|
372
415
|
operations: [
|
|
373
416
|
{
|
|
@@ -377,13 +420,19 @@ describe("prefetchData", () => {
|
|
|
377
420
|
body: "query Product($id: ID!) {\n product(id: $id) {\n id\n title\n rating\n price\n description\n images\n __typename\n }\n}",
|
|
378
421
|
variables: { id: "ID" },
|
|
379
422
|
prefetch: false,
|
|
380
|
-
tools: [
|
|
423
|
+
tools: [
|
|
424
|
+
{
|
|
425
|
+
name: "Get Product",
|
|
426
|
+
description: "Shows the details page for a specific product.",
|
|
427
|
+
},
|
|
428
|
+
],
|
|
381
429
|
},
|
|
382
430
|
],
|
|
383
431
|
resource: "index.html",
|
|
384
432
|
};
|
|
385
433
|
|
|
386
434
|
const client = new ExtendedApolloClient({
|
|
435
|
+
cache: new InMemoryCache(),
|
|
387
436
|
manifest: manifest as ApplicationManifest,
|
|
388
437
|
});
|
|
389
438
|
await client.prefetchData();
|
|
@@ -409,3 +458,167 @@ describe("prefetchData", () => {
|
|
|
409
458
|
`);
|
|
410
459
|
});
|
|
411
460
|
});
|
|
461
|
+
|
|
462
|
+
describe("custom links", () => {
|
|
463
|
+
test("allows for custom links provided to the constructor", async () => {
|
|
464
|
+
vi.stubGlobal("openai", {
|
|
465
|
+
toolInput: {},
|
|
466
|
+
toolOutput: {},
|
|
467
|
+
toolResponseMetadata: {
|
|
468
|
+
toolName: "the-store--Get Product",
|
|
469
|
+
},
|
|
470
|
+
callTool: vi.fn(async (name: string, args: Record<string, unknown>) => {
|
|
471
|
+
return {
|
|
472
|
+
structuredContent: {
|
|
473
|
+
data: {
|
|
474
|
+
product: {
|
|
475
|
+
id: "1",
|
|
476
|
+
title: "Pen",
|
|
477
|
+
rating: 5,
|
|
478
|
+
price: 1.0,
|
|
479
|
+
description: "Awesome pen",
|
|
480
|
+
images: [],
|
|
481
|
+
__typename: "Product",
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
}),
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const manifest = createManifest();
|
|
490
|
+
const linkHandler = vi.fn<ApolloLink.RequestHandler>((operation, forward) =>
|
|
491
|
+
forward(operation)
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
const client = new ApolloClient({
|
|
495
|
+
manifest,
|
|
496
|
+
cache: new InMemoryCache(),
|
|
497
|
+
link: ApolloLink.from([new ApolloLink(linkHandler), new ToolCallLink()]),
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const variables = { id: "1" };
|
|
501
|
+
const query = parse(manifest.operations[0].body);
|
|
502
|
+
|
|
503
|
+
await expect(client.query({ query, variables })).resolves.toStrictEqual({
|
|
504
|
+
data: {
|
|
505
|
+
product: {
|
|
506
|
+
id: "1",
|
|
507
|
+
title: "Pen",
|
|
508
|
+
rating: 5,
|
|
509
|
+
price: 1.0,
|
|
510
|
+
description: "Awesome pen",
|
|
511
|
+
images: [],
|
|
512
|
+
__typename: "Product",
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
expect(linkHandler).toHaveBeenCalledOnce();
|
|
518
|
+
expect(linkHandler).toHaveBeenCalledWith(
|
|
519
|
+
expect.objectContaining({ query, variables, operationType: "query" }),
|
|
520
|
+
expect.any(Function)
|
|
521
|
+
);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("enforces ToolCallLink as terminating link", async () => {
|
|
525
|
+
const manifest = createManifest();
|
|
526
|
+
const expectedError = new Error(
|
|
527
|
+
"The terminating link must be a `ToolCallLink`. If you are using a `split` link, ensure the `right` branch uses a `ToolCallLink` as the terminating link."
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
expect(() => {
|
|
531
|
+
new ApolloClient({
|
|
532
|
+
manifest,
|
|
533
|
+
cache: new InMemoryCache(),
|
|
534
|
+
link: new HttpLink(),
|
|
535
|
+
});
|
|
536
|
+
}).toThrow(expectedError);
|
|
537
|
+
|
|
538
|
+
expect(() => {
|
|
539
|
+
new ApolloClient({
|
|
540
|
+
manifest,
|
|
541
|
+
cache: new InMemoryCache(),
|
|
542
|
+
link: new ApolloLink(),
|
|
543
|
+
});
|
|
544
|
+
}).toThrow(expectedError);
|
|
545
|
+
|
|
546
|
+
expect(() => {
|
|
547
|
+
new ApolloClient({
|
|
548
|
+
manifest,
|
|
549
|
+
cache: new InMemoryCache(),
|
|
550
|
+
link: ApolloLink.from([new ApolloLink(), new HttpLink()]),
|
|
551
|
+
});
|
|
552
|
+
}).toThrow(expectedError);
|
|
553
|
+
|
|
554
|
+
expect(() => {
|
|
555
|
+
new ApolloClient({
|
|
556
|
+
manifest,
|
|
557
|
+
cache: new InMemoryCache(),
|
|
558
|
+
link: ApolloLink.split(() => true, new ApolloLink(), new HttpLink()),
|
|
559
|
+
});
|
|
560
|
+
}).toThrow(expectedError);
|
|
561
|
+
|
|
562
|
+
expect(() => {
|
|
563
|
+
new ApolloClient({
|
|
564
|
+
manifest,
|
|
565
|
+
cache: new InMemoryCache(),
|
|
566
|
+
link: ApolloLink.split(() => true, new ToolCallLink(), new HttpLink()),
|
|
567
|
+
});
|
|
568
|
+
}).toThrow(expectedError);
|
|
569
|
+
|
|
570
|
+
// Allow you to use a custom terminating link for `split` links if the
|
|
571
|
+
// custom link is the `left` branch link.
|
|
572
|
+
expect(() => {
|
|
573
|
+
new ApolloClient({
|
|
574
|
+
manifest,
|
|
575
|
+
cache: new InMemoryCache(),
|
|
576
|
+
link: ApolloLink.split(() => true, new HttpLink(), new ToolCallLink()),
|
|
577
|
+
});
|
|
578
|
+
}).not.toThrow(expectedError);
|
|
579
|
+
|
|
580
|
+
expect(() => {
|
|
581
|
+
new ApolloClient({
|
|
582
|
+
manifest,
|
|
583
|
+
cache: new InMemoryCache(),
|
|
584
|
+
link: ApolloLink.split(
|
|
585
|
+
() => true,
|
|
586
|
+
new ToolCallLink(),
|
|
587
|
+
new ToolCallLink()
|
|
588
|
+
),
|
|
589
|
+
});
|
|
590
|
+
}).not.toThrow();
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
function createManifest(
|
|
595
|
+
overrides?: Partial<ApplicationManifest>
|
|
596
|
+
): ApplicationManifest {
|
|
597
|
+
return {
|
|
598
|
+
format: "apollo-ai-app-manifest",
|
|
599
|
+
version: "1",
|
|
600
|
+
name: "the-store",
|
|
601
|
+
description:
|
|
602
|
+
"An online store selling a variety of high quality products across many different categories.",
|
|
603
|
+
hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
|
|
604
|
+
operations: [
|
|
605
|
+
{
|
|
606
|
+
id: "c43af26552874026c3fb346148c5795896aa2f3a872410a0a2621cffee25291c",
|
|
607
|
+
name: "Product",
|
|
608
|
+
type: "query",
|
|
609
|
+
body: "query Product($id: ID!) {\n product(id: $id) {\n id\n title\n rating\n price\n description\n images\n __typename\n }\n}",
|
|
610
|
+
variables: { id: "ID" },
|
|
611
|
+
prefetch: false,
|
|
612
|
+
tools: [
|
|
613
|
+
{
|
|
614
|
+
name: "Get Product",
|
|
615
|
+
description: "Shows the details page for a specific product.",
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
},
|
|
619
|
+
],
|
|
620
|
+
resource: "index.html",
|
|
621
|
+
csp: { resourceDomains: [], connectDomains: [] },
|
|
622
|
+
...overrides,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { selectHttpOptionsAndBody } from "@apollo/client/link/http";
|
|
4
|
-
import { fallbackHttpConfig } from "@apollo/client/link/http";
|
|
1
|
+
import type { ApolloLink } from "@apollo/client";
|
|
2
|
+
import { ApolloClient } from "@apollo/client";
|
|
5
3
|
import { DocumentTransform } from "@apollo/client";
|
|
6
4
|
import { removeDirectivesFromDocument } from "@apollo/client/utilities/internal";
|
|
7
5
|
import { parse } from "graphql";
|
|
6
|
+
import { __DEV__ } from "@apollo/client/utilities/environment";
|
|
8
7
|
import "../types/openai";
|
|
9
8
|
import { ApplicationManifest } from "../types/application-manifest";
|
|
9
|
+
import { ToolCallLink } from "./link/ToolCallLink";
|
|
10
10
|
|
|
11
11
|
// TODO: In the future if/when we support PQs again, do pqLink.concat(toolCallLink)
|
|
12
12
|
// Commenting this out for now.
|
|
@@ -16,27 +16,9 @@ import { ApplicationManifest } from "../types/application-manifest";
|
|
|
16
16
|
// sha256: (queryString) => sha256(queryString),
|
|
17
17
|
// });
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
const toolCallLink = new ApolloLink((operation) => {
|
|
22
|
-
const context = operation.getContext();
|
|
23
|
-
const contextConfig = {
|
|
24
|
-
http: context.http,
|
|
25
|
-
options: context.fetchOptions,
|
|
26
|
-
credentials: context.credentials,
|
|
27
|
-
headers: context.headers,
|
|
28
|
-
};
|
|
29
|
-
const { query, variables } = selectHttpOptionsAndBody(operation, fallbackHttpConfig, contextConfig).body;
|
|
30
|
-
|
|
31
|
-
return Observable.from(window.openai.callTool("execute", { query, variables })).pipe(
|
|
32
|
-
Observable.map((result) => ({ data: result.structuredContent.data }))
|
|
33
|
-
);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// This allows us to extend the options with the "manifest" option AND make link/cache optional (they are normally required)
|
|
37
|
-
type ExtendedApolloClientOptions = Omit<ApolloClient.Options, "link" | "cache"> & {
|
|
19
|
+
// This allows us to extend the options with the "manifest" option AND make link optional (it is normally required)
|
|
20
|
+
type ExtendedApolloClientOptions = Omit<ApolloClient.Options, "link"> & {
|
|
38
21
|
link?: ApolloClient.Options["link"];
|
|
39
|
-
cache?: ApolloClient.Options["cache"];
|
|
40
22
|
manifest: ApplicationManifest;
|
|
41
23
|
};
|
|
42
24
|
|
|
@@ -44,12 +26,21 @@ export class ExtendedApolloClient extends ApolloClient {
|
|
|
44
26
|
manifest: ApplicationManifest;
|
|
45
27
|
|
|
46
28
|
constructor(options: ExtendedApolloClientOptions) {
|
|
29
|
+
const link = options.link ?? new ToolCallLink();
|
|
30
|
+
|
|
31
|
+
if (__DEV__) {
|
|
32
|
+
validateTerminatingLink(link);
|
|
33
|
+
}
|
|
34
|
+
|
|
47
35
|
super({
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
...options,
|
|
37
|
+
link,
|
|
50
38
|
// Strip out the prefetch/tool directives so they don't get sent with the operation to the server
|
|
51
39
|
documentTransform: new DocumentTransform((document) => {
|
|
52
|
-
return removeDirectivesFromDocument(
|
|
40
|
+
return removeDirectivesFromDocument(
|
|
41
|
+
[{ name: "prefetch" }, { name: "tool" }],
|
|
42
|
+
document
|
|
43
|
+
)!;
|
|
53
44
|
}),
|
|
54
45
|
});
|
|
55
46
|
|
|
@@ -59,7 +50,11 @@ export class ExtendedApolloClient extends ApolloClient {
|
|
|
59
50
|
async prefetchData() {
|
|
60
51
|
// Write prefetched data to the cache
|
|
61
52
|
this.manifest.operations.forEach((operation) => {
|
|
62
|
-
if (
|
|
53
|
+
if (
|
|
54
|
+
operation.prefetch &&
|
|
55
|
+
operation.prefetchID &&
|
|
56
|
+
window.openai.toolOutput.prefetch?.[operation.prefetchID]
|
|
57
|
+
) {
|
|
63
58
|
this.writeQuery({
|
|
64
59
|
query: parse(operation.body),
|
|
65
60
|
data: window.openai.toolOutput.prefetch[operation.prefetchID].data,
|
|
@@ -69,13 +64,18 @@ export class ExtendedApolloClient extends ApolloClient {
|
|
|
69
64
|
// If this operation has the tool that matches up with the tool that was executed, write the tool result to the cache
|
|
70
65
|
if (
|
|
71
66
|
operation.tools?.find(
|
|
72
|
-
(tool) =>
|
|
67
|
+
(tool) =>
|
|
68
|
+
`${this.manifest.name}--${tool.name}` ===
|
|
69
|
+
window.openai.toolResponseMetadata.toolName
|
|
73
70
|
)
|
|
74
71
|
) {
|
|
75
72
|
// We need to include the variables that were used as part of the tool call so that we get a proper cache entry
|
|
76
73
|
// However, we only want to include toolInput's that were graphql operation (ignore extraInputs)
|
|
77
74
|
const variables = Object.keys(window.openai.toolInput).reduce(
|
|
78
|
-
(obj, key) =>
|
|
75
|
+
(obj, key) =>
|
|
76
|
+
operation.variables[key] ?
|
|
77
|
+
{ ...obj, [key]: window.openai.toolInput[key] }
|
|
78
|
+
: obj,
|
|
79
79
|
{}
|
|
80
80
|
);
|
|
81
81
|
|
|
@@ -88,3 +88,17 @@ export class ExtendedApolloClient extends ApolloClient {
|
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
function validateTerminatingLink(link: ApolloLink) {
|
|
93
|
+
let terminatingLink = link;
|
|
94
|
+
|
|
95
|
+
while (terminatingLink.right) {
|
|
96
|
+
terminatingLink = terminatingLink.right;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (terminatingLink.constructor.name !== "ToolCallLink") {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"The terminating link must be a `ToolCallLink`. If you are using a `split` link, ensure the `right` branch uses a `ToolCallLink` as the terminating link."
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ApolloLink, Observable } from "@apollo/client";
|
|
2
|
+
import { from, map } from "rxjs";
|
|
3
|
+
import {
|
|
4
|
+
fallbackHttpConfig,
|
|
5
|
+
selectHttpOptionsAndBody,
|
|
6
|
+
} from "@apollo/client/link/http";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A terminating link that sends a GraphQL request through an agent tool call.
|
|
10
|
+
* When providing a custom link chain to `ApolloClient`, `ApolloClient` will
|
|
11
|
+
* validate that the terminating link is an instance of this link.
|
|
12
|
+
*
|
|
13
|
+
* @example Provding a custom link chain
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { ApolloLink } from "@apollo/client";
|
|
17
|
+
* import { ApolloClient, ToolCallLink } from "@apollo/client-ai-apps";
|
|
18
|
+
*
|
|
19
|
+
* const link = ApolloLink.from([
|
|
20
|
+
* ...otherLinks,
|
|
21
|
+
* new ToolCallLink()
|
|
22
|
+
* ]);
|
|
23
|
+
*
|
|
24
|
+
* const client = new ApolloClient({
|
|
25
|
+
* link,
|
|
26
|
+
* // ...
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export class ToolCallLink extends ApolloLink {
|
|
31
|
+
request(operation: ApolloLink.Operation): Observable<ApolloLink.Result> {
|
|
32
|
+
const context = operation.getContext();
|
|
33
|
+
const contextConfig = {
|
|
34
|
+
http: context.http,
|
|
35
|
+
options: context.fetchOptions,
|
|
36
|
+
credentials: context.credentials,
|
|
37
|
+
headers: context.headers,
|
|
38
|
+
};
|
|
39
|
+
const { query, variables } = selectHttpOptionsAndBody(
|
|
40
|
+
operation,
|
|
41
|
+
fallbackHttpConfig,
|
|
42
|
+
contextConfig
|
|
43
|
+
).body;
|
|
44
|
+
|
|
45
|
+
return from(window.openai.callTool("execute", { query, variables })).pipe(
|
|
46
|
+
map((result) => ({ data: result.structuredContent.data }))
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -26,7 +26,13 @@ export const ExtendedApolloProvider = ({
|
|
|
26
26
|
if (window.openai?.toolOutput) {
|
|
27
27
|
window.dispatchEvent(new CustomEvent(SET_GLOBALS_EVENT_TYPE));
|
|
28
28
|
}
|
|
29
|
-
}, [setHasPreloaded]);
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
return () => {
|
|
31
|
+
window.removeEventListener(SET_GLOBALS_EVENT_TYPE, prefetchData);
|
|
32
|
+
};
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
return hasPreloaded ?
|
|
36
|
+
<ApolloProvider client={client}>{children}</ApolloProvider>
|
|
37
|
+
: null;
|
|
32
38
|
};
|
package/src/hooks/useCallTool.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
type UseCallToolResult = <K>(
|
|
1
|
+
type UseCallToolResult = <K>(
|
|
2
|
+
toolId: string,
|
|
3
|
+
variables?: Record<string, unknown> | undefined
|
|
4
|
+
) => Promise<K>;
|
|
2
5
|
|
|
3
6
|
export const useCallTool = (): UseCallToolResult => {
|
|
4
|
-
const callTool = async (
|
|
5
|
-
|
|
7
|
+
const callTool = async (
|
|
8
|
+
toolId: string,
|
|
9
|
+
variables: Record<string, unknown> | undefined = {}
|
|
10
|
+
) => await window.openai?.callTool(toolId, variables);
|
|
6
11
|
|
|
7
12
|
return callTool;
|
|
8
13
|
};
|
|
@@ -17,7 +17,9 @@ test("Should update value when globals are updated and event it triggered", asyn
|
|
|
17
17
|
});
|
|
18
18
|
window.dispatchEvent(
|
|
19
19
|
new CustomEvent(SET_GLOBALS_EVENT_TYPE, {
|
|
20
|
-
detail: {
|
|
20
|
+
detail: {
|
|
21
|
+
globals: { toolResponseMetadata: { toolName: "my-other-tool" } },
|
|
22
|
+
},
|
|
21
23
|
})
|
|
22
24
|
);
|
|
23
25
|
});
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { useSyncExternalStore } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
SET_GLOBALS_EVENT_TYPE,
|
|
4
|
+
SetGlobalsEvent,
|
|
5
|
+
OpenAiGlobals,
|
|
6
|
+
} from "../types/openai";
|
|
3
7
|
|
|
4
|
-
export function useOpenAiGlobal<K extends keyof OpenAiGlobals>(
|
|
8
|
+
export function useOpenAiGlobal<K extends keyof OpenAiGlobals>(
|
|
9
|
+
key: K
|
|
10
|
+
): OpenAiGlobals[K] {
|
|
5
11
|
return useSyncExternalStore(
|
|
6
12
|
(onChange) => {
|
|
7
13
|
const handleSetGlobal = (event: SetGlobalsEvent) => {
|
|
@@ -9,5 +9,7 @@ test("Should set display mode when returned function is called", async () => {
|
|
|
9
9
|
const sendFollowUpMessage = useSendFollowUpMessage();
|
|
10
10
|
await sendFollowUpMessage("Do a cool thing!");
|
|
11
11
|
|
|
12
|
-
expect(window.openai.sendFollowUpMessage).toBeCalledWith({
|
|
12
|
+
expect(window.openai.sendFollowUpMessage).toBeCalledWith({
|
|
13
|
+
prompt: "Do a cool thing!",
|
|
14
|
+
});
|
|
13
15
|
});
|
|
@@ -7,9 +7,13 @@ test("Should trigger effect when tool name matches toolResponseMetadata", async
|
|
|
7
7
|
toolResponseMetadata: { toolName: "my-app--my-tool" },
|
|
8
8
|
});
|
|
9
9
|
const navigate = vi.fn();
|
|
10
|
-
const wrapper = ({ children }: { children: any }) =>
|
|
10
|
+
const wrapper = ({ children }: { children: any }) => (
|
|
11
|
+
<ToolUseProvider appName="my-app">{children}</ToolUseProvider>
|
|
12
|
+
);
|
|
11
13
|
|
|
12
|
-
renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]), {
|
|
14
|
+
renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]), {
|
|
15
|
+
wrapper,
|
|
16
|
+
});
|
|
13
17
|
|
|
14
18
|
expect(navigate).toBeCalled();
|
|
15
19
|
});
|
|
@@ -19,9 +23,17 @@ test("Should trigger effect when one of multiple tool name matches toolResponseM
|
|
|
19
23
|
toolResponseMetadata: { toolName: "my-app--my-tool" },
|
|
20
24
|
});
|
|
21
25
|
const navigate = vi.fn();
|
|
22
|
-
const wrapper = ({ children }: { children: any }) =>
|
|
26
|
+
const wrapper = ({ children }: { children: any }) => (
|
|
27
|
+
<ToolUseProvider appName="my-app">{children}</ToolUseProvider>
|
|
28
|
+
);
|
|
23
29
|
|
|
24
|
-
renderHook(
|
|
30
|
+
renderHook(
|
|
31
|
+
() =>
|
|
32
|
+
useToolEffect(["my-tool", "my-similar-tool"], () => navigate(), [
|
|
33
|
+
navigate,
|
|
34
|
+
]),
|
|
35
|
+
{ wrapper }
|
|
36
|
+
);
|
|
25
37
|
|
|
26
38
|
expect(navigate).toBeCalled();
|
|
27
39
|
});
|
|
@@ -31,9 +43,13 @@ test("Should not trigger effect when tool name does not match toolResponseMetada
|
|
|
31
43
|
toolResponseMetadata: { toolName: "my-app--my-other-tool" },
|
|
32
44
|
});
|
|
33
45
|
const navigate = vi.fn();
|
|
34
|
-
const wrapper = ({ children }: { children: any }) =>
|
|
46
|
+
const wrapper = ({ children }: { children: any }) => (
|
|
47
|
+
<ToolUseProvider appName="my-app">{children}</ToolUseProvider>
|
|
48
|
+
);
|
|
35
49
|
|
|
36
|
-
renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]), {
|
|
50
|
+
renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]), {
|
|
51
|
+
wrapper,
|
|
52
|
+
});
|
|
37
53
|
|
|
38
54
|
expect(navigate).not.toBeCalled();
|
|
39
55
|
});
|
|
@@ -44,7 +60,7 @@ test("Should throw an error when used outside of a ToolUseProvider", async () =>
|
|
|
44
60
|
});
|
|
45
61
|
const navigate = vi.fn();
|
|
46
62
|
|
|
47
|
-
expect(() =>
|
|
48
|
-
"
|
|
49
|
-
);
|
|
63
|
+
expect(() =>
|
|
64
|
+
renderHook(() => useToolEffect("my-tool", () => navigate(), [navigate]))
|
|
65
|
+
).toThrowError("useToolEffect must be used within ToolUseProvider");
|
|
50
66
|
});
|