@apollo/client-ai-apps 0.1.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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/pr.yaml +24 -0
- package/.github/workflows/release.yaml +36 -0
- package/LICENSE +21 -0
- package/dist/apollo_client/client.d.ts +14 -0
- package/dist/apollo_client/provider.d.ts +5 -0
- package/dist/hooks/useOpenAiGlobal.d.ts +2 -0
- package/dist/hooks/useRequestDisplayMode.d.ts +4 -0
- package/dist/hooks/useSendFollowUpMessage.d.ts +1 -0
- package/dist/hooks/useToolEffect.d.ts +6 -0
- package/dist/hooks/useToolInput.d.ts +1 -0
- package/dist/hooks/useToolName.d.ts +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +177 -0
- package/dist/types/application-manifest.d.ts +29 -0
- package/dist/types/openai.d.ts +73 -0
- package/dist/vite/index.d.ts +1 -0
- package/dist/vite/index.js +210 -0
- package/dist/vite/operation_manifest_plugin.d.ts +10 -0
- package/package.json +53 -0
- package/scripts/build-vite.mjs +18 -0
- package/scripts/build.mjs +7 -0
- package/scripts/dev.mjs +21 -0
- package/scripts/shared.mjs +9 -0
- package/src/apollo_client/client.test.ts +411 -0
- package/src/apollo_client/client.ts +90 -0
- package/src/apollo_client/provider.test.tsx +41 -0
- package/src/apollo_client/provider.tsx +32 -0
- package/src/hooks/useCallTool.test.ts +46 -0
- package/src/hooks/useCallTool.ts +8 -0
- package/src/hooks/useOpenAiGlobal.test.ts +54 -0
- package/src/hooks/useOpenAiGlobal.ts +26 -0
- package/src/hooks/useRequestDisplayMode.ts +7 -0
- package/src/hooks/useSendFollowUpMessage.ts +7 -0
- package/src/hooks/useToolEffect.tsx +41 -0
- package/src/hooks/useToolInput.ts +7 -0
- package/src/hooks/useToolName.ts +7 -0
- package/src/index.ts +12 -0
- package/src/types/application-manifest.ts +32 -0
- package/src/types/openai.ts +90 -0
- package/src/vite/index.ts +1 -0
- package/src/vite/operation_manifest_plugin.ts +274 -0
- package/vitest-setup.ts +1 -0
- package/vitest.config.ts +12 -0
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@apollo/client-ai-apps",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"module": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/index.js",
|
|
13
|
+
"./vite": "./dist/vite/index.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "node ./scripts/dev.mjs",
|
|
17
|
+
"build": "npm run build:react && npm run build:vite",
|
|
18
|
+
"build:react": "node ./scripts/build.mjs && tsc src/index.ts --emitDeclarationOnly --declaration --outDir dist --skipLibCheck --lib ES2015,DOM --target ES2015 --moduleResolution bundler --jsx react",
|
|
19
|
+
"build:vite": "node ./scripts/build-vite.mjs && tsc src/vite/index.ts --emitDeclarationOnly --declaration --outDir dist/vite --skipLibCheck --lib ES2015,DOM --target ES2020 --module esnext --moduleResolution node --allowSyntheticDefaultImports",
|
|
20
|
+
"changeset:new": "npx @changesets/cli",
|
|
21
|
+
"changeset:version": "npx @changesets/cli version",
|
|
22
|
+
"test": "vitest run --coverage",
|
|
23
|
+
"test:watch": "vitest --coverage"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "ISC",
|
|
28
|
+
"description": "",
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
31
|
+
"@testing-library/react": "^16.3.0",
|
|
32
|
+
"@types/node": "^24.10.0",
|
|
33
|
+
"@types/react": "^19.2.2",
|
|
34
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
35
|
+
"@vitest/coverage-v8": "^4.0.13",
|
|
36
|
+
"esbuild": "^0.25.12",
|
|
37
|
+
"graphql": "^16.9.0",
|
|
38
|
+
"happy-dom": "^20.0.10",
|
|
39
|
+
"rxjs": "^7.8.1",
|
|
40
|
+
"typescript": "^5.9.3",
|
|
41
|
+
"vitest": "^4.0.13"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": "^19.2.0",
|
|
45
|
+
"react-dom": "^19.2.0"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@apollo/client": "^4.0.9",
|
|
49
|
+
"@graphql-tools/graphql-tag-pluck": "^8.3.23",
|
|
50
|
+
"crypto-hash": "^4.0.0",
|
|
51
|
+
"glob": "^11.0.3"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as esbuild from "esbuild";
|
|
2
|
+
|
|
3
|
+
// Separately build Vite plugin
|
|
4
|
+
await esbuild.build({
|
|
5
|
+
entryPoints: ["src/vite/index.ts"],
|
|
6
|
+
bundle: true,
|
|
7
|
+
outdir: "dist/vite",
|
|
8
|
+
platform: "node",
|
|
9
|
+
format: "esm",
|
|
10
|
+
external: [
|
|
11
|
+
"glob",
|
|
12
|
+
"@graphql-tools/graphql-tag-pluck",
|
|
13
|
+
"@graphql-tools/graphql-tag-pluck",
|
|
14
|
+
"graphql",
|
|
15
|
+
"@apollo/client",
|
|
16
|
+
"rxjs",
|
|
17
|
+
],
|
|
18
|
+
});
|
package/scripts/dev.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as esbuild from "esbuild";
|
|
2
|
+
import { sharedConfig } from "./shared.mjs";
|
|
3
|
+
|
|
4
|
+
console.log("Starting dev mode...");
|
|
5
|
+
|
|
6
|
+
let ctx = await esbuild.context({
|
|
7
|
+
...sharedConfig,
|
|
8
|
+
plugins: [
|
|
9
|
+
{
|
|
10
|
+
name: "rebuild-logger",
|
|
11
|
+
setup(build) {
|
|
12
|
+
build.onEnd((result) => {
|
|
13
|
+
console.log(`Rebuilt at ${new Date().toLocaleTimeString()} (${result.errors.length} errors)`);
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
console.log("Watching for changes...");
|
|
21
|
+
await ctx.watch();
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { expect, test, describe, vi } from "vitest";
|
|
2
|
+
import { ExtendedApolloClient } from "./client";
|
|
3
|
+
import { ApplicationManifest } from "../types/application-manifest";
|
|
4
|
+
import { parse } from "graphql";
|
|
5
|
+
|
|
6
|
+
describe("Client Basics", () => {
|
|
7
|
+
test("Should execute tool call when client.query is called", async () => {
|
|
8
|
+
vi.stubGlobal("openai", {
|
|
9
|
+
toolInput: {},
|
|
10
|
+
toolOutput: {},
|
|
11
|
+
toolResponseMetadata: {
|
|
12
|
+
toolName: "the-store--Get Product",
|
|
13
|
+
},
|
|
14
|
+
callTool: vi.fn(async (name: string, args: Record<string, unknown>) => {
|
|
15
|
+
return {
|
|
16
|
+
structuredContent: {
|
|
17
|
+
data: {
|
|
18
|
+
product: {
|
|
19
|
+
id: "1",
|
|
20
|
+
title: "Pen",
|
|
21
|
+
rating: 5,
|
|
22
|
+
price: 1.0,
|
|
23
|
+
description: "Awesome pen",
|
|
24
|
+
images: [],
|
|
25
|
+
__typename: "Product",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const manifest = {
|
|
34
|
+
format: "apollo-ai-app-manifest",
|
|
35
|
+
version: "1",
|
|
36
|
+
name: "the-store",
|
|
37
|
+
description: "An online store selling a variety of high quality products across many different categories.",
|
|
38
|
+
hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
|
|
39
|
+
operations: [
|
|
40
|
+
{
|
|
41
|
+
id: "c43af26552874026c3fb346148c5795896aa2f3a872410a0a2621cffee25291c",
|
|
42
|
+
name: "Product",
|
|
43
|
+
type: "query",
|
|
44
|
+
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
|
+
variables: { id: "ID" },
|
|
46
|
+
prefetch: false,
|
|
47
|
+
tools: [{ name: "Get Product", description: "Shows the details page for a specific product." }],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
resource: "index.html",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const client = new ExtendedApolloClient({
|
|
54
|
+
manifest: manifest as ApplicationManifest,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const variables = { id: "1" };
|
|
58
|
+
await client.query({ query: parse(manifest.operations[0].body), variables });
|
|
59
|
+
|
|
60
|
+
expect(window.openai.callTool).toBeCalledWith("execute", { query: manifest.operations[0].body, variables });
|
|
61
|
+
expect(client.extract()).toMatchInlineSnapshot(`
|
|
62
|
+
{
|
|
63
|
+
"Product:1": {
|
|
64
|
+
"__typename": "Product",
|
|
65
|
+
"description": "Awesome pen",
|
|
66
|
+
"id": "1",
|
|
67
|
+
"images": [],
|
|
68
|
+
"price": 1,
|
|
69
|
+
"rating": 5,
|
|
70
|
+
"title": "Pen",
|
|
71
|
+
},
|
|
72
|
+
"ROOT_QUERY": {
|
|
73
|
+
"__typename": "Query",
|
|
74
|
+
"product({"id":"1"})": {
|
|
75
|
+
"__ref": "Product:1",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
`);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("prefetchData", () => {
|
|
84
|
+
test("Should cache tool response when data is provided", async () => {
|
|
85
|
+
vi.stubGlobal("openai", {
|
|
86
|
+
toolInput: {
|
|
87
|
+
id: 1,
|
|
88
|
+
},
|
|
89
|
+
toolOutput: {
|
|
90
|
+
result: {
|
|
91
|
+
data: {
|
|
92
|
+
product: {
|
|
93
|
+
id: "1",
|
|
94
|
+
title: "Pen",
|
|
95
|
+
rating: 5,
|
|
96
|
+
price: 1.0,
|
|
97
|
+
description: "Awesome pen",
|
|
98
|
+
images: [],
|
|
99
|
+
__typename: "Product",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
toolResponseMetadata: {
|
|
105
|
+
toolName: "the-store--Get Product",
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const manifest = {
|
|
110
|
+
format: "apollo-ai-app-manifest",
|
|
111
|
+
version: "1",
|
|
112
|
+
name: "the-store",
|
|
113
|
+
description: "An online store selling a variety of high quality products across many different categories.",
|
|
114
|
+
hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
|
|
115
|
+
operations: [
|
|
116
|
+
{
|
|
117
|
+
id: "c43af26552874026c3fb346148c5795896aa2f3a872410a0a2621cffee25291c",
|
|
118
|
+
name: "Product",
|
|
119
|
+
type: "query",
|
|
120
|
+
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
|
+
variables: { id: "ID" },
|
|
122
|
+
prefetch: false,
|
|
123
|
+
tools: [{ name: "Get Product", description: "Shows the details page for a specific product." }],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
resource: "index.html",
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const client = new ExtendedApolloClient({
|
|
130
|
+
manifest: manifest as ApplicationManifest,
|
|
131
|
+
});
|
|
132
|
+
await client.prefetchData();
|
|
133
|
+
|
|
134
|
+
expect(client.extract()).toMatchInlineSnapshot(`
|
|
135
|
+
{
|
|
136
|
+
"Product:1": {
|
|
137
|
+
"__typename": "Product",
|
|
138
|
+
"description": "Awesome pen",
|
|
139
|
+
"id": "1",
|
|
140
|
+
"images": [],
|
|
141
|
+
"price": 1,
|
|
142
|
+
"rating": 5,
|
|
143
|
+
"title": "Pen",
|
|
144
|
+
},
|
|
145
|
+
"ROOT_QUERY": {
|
|
146
|
+
"__typename": "Query",
|
|
147
|
+
"product({"id":1})": {
|
|
148
|
+
"__ref": "Product:1",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
`);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("Should cache prefetched data when prefetched data is provided", async () => {
|
|
156
|
+
vi.stubGlobal("openai", {
|
|
157
|
+
toolInput: {},
|
|
158
|
+
toolOutput: {
|
|
159
|
+
result: {},
|
|
160
|
+
prefetch: {
|
|
161
|
+
__anonymous: {
|
|
162
|
+
data: {
|
|
163
|
+
topProducts: [
|
|
164
|
+
{
|
|
165
|
+
id: "2",
|
|
166
|
+
title: "iPhone 17",
|
|
167
|
+
rating: 5,
|
|
168
|
+
price: 999.99,
|
|
169
|
+
description: "Awesome phone",
|
|
170
|
+
images: [],
|
|
171
|
+
__typename: "Product",
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
toolResponseMetadata: {
|
|
179
|
+
toolName: "the-store--Get Product",
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const manifest = {
|
|
184
|
+
format: "apollo-ai-app-manifest",
|
|
185
|
+
version: "1",
|
|
186
|
+
name: "the-store",
|
|
187
|
+
description: "An online store selling a variety of high quality products across many different categories.",
|
|
188
|
+
hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
|
|
189
|
+
operations: [
|
|
190
|
+
{
|
|
191
|
+
id: "cd0d52159b9003e791de97c6a76efa03d34fe00cee278d1a3f4bfcec5fb3e1e6",
|
|
192
|
+
name: "TopProducts",
|
|
193
|
+
type: "query",
|
|
194
|
+
body: "query TopProducts {\n topProducts {\n id\n title\n rating\n price\n __typename\n }\n}",
|
|
195
|
+
variables: {},
|
|
196
|
+
prefetch: true,
|
|
197
|
+
prefetchID: "__anonymous",
|
|
198
|
+
tools: [{ name: "Top Products", description: "Shows the currently highest rated products." }],
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
resource: "index.html",
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const client = new ExtendedApolloClient({
|
|
205
|
+
manifest: manifest as ApplicationManifest,
|
|
206
|
+
});
|
|
207
|
+
await client.prefetchData();
|
|
208
|
+
|
|
209
|
+
expect(client.extract()).toMatchInlineSnapshot(`
|
|
210
|
+
{
|
|
211
|
+
"Product:2": {
|
|
212
|
+
"__typename": "Product",
|
|
213
|
+
"id": "2",
|
|
214
|
+
"price": 999.99,
|
|
215
|
+
"rating": 5,
|
|
216
|
+
"title": "iPhone 17",
|
|
217
|
+
},
|
|
218
|
+
"ROOT_QUERY": {
|
|
219
|
+
"__typename": "Query",
|
|
220
|
+
"topProducts": [
|
|
221
|
+
{
|
|
222
|
+
"__ref": "Product:2",
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
`);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("Should cache both prefetch and tool response when both are provided", async () => {
|
|
231
|
+
vi.stubGlobal("openai", {
|
|
232
|
+
toolInput: {
|
|
233
|
+
id: 1,
|
|
234
|
+
},
|
|
235
|
+
toolOutput: {
|
|
236
|
+
result: {
|
|
237
|
+
data: {
|
|
238
|
+
product: {
|
|
239
|
+
id: "1",
|
|
240
|
+
title: "Pen",
|
|
241
|
+
rating: 5,
|
|
242
|
+
price: 1.0,
|
|
243
|
+
description: "Awesome pen",
|
|
244
|
+
images: [],
|
|
245
|
+
__typename: "Product",
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
prefetch: {
|
|
250
|
+
__anonymous: {
|
|
251
|
+
data: {
|
|
252
|
+
topProducts: [
|
|
253
|
+
{
|
|
254
|
+
id: "2",
|
|
255
|
+
title: "iPhone 17",
|
|
256
|
+
rating: 5,
|
|
257
|
+
price: 999.99,
|
|
258
|
+
description: "Awesome phone",
|
|
259
|
+
images: [],
|
|
260
|
+
__typename: "Product",
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
toolResponseMetadata: {
|
|
268
|
+
toolName: "the-store--Get Product",
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const manifest = {
|
|
273
|
+
format: "apollo-ai-app-manifest",
|
|
274
|
+
version: "1",
|
|
275
|
+
name: "the-store",
|
|
276
|
+
description: "An online store selling a variety of high quality products across many different categories.",
|
|
277
|
+
hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
|
|
278
|
+
operations: [
|
|
279
|
+
{
|
|
280
|
+
id: "c43af26552874026c3fb346148c5795896aa2f3a872410a0a2621cffee25291c",
|
|
281
|
+
name: "Product",
|
|
282
|
+
type: "query",
|
|
283
|
+
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
|
+
variables: { id: "ID" },
|
|
285
|
+
prefetch: false,
|
|
286
|
+
tools: [{ name: "Get Product", description: "Shows the details page for a specific product." }],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "cd0d52159b9003e791de97c6a76efa03d34fe00cee278d1a3f4bfcec5fb3e1e6",
|
|
290
|
+
name: "TopProducts",
|
|
291
|
+
type: "query",
|
|
292
|
+
body: "query TopProducts {\n topProducts {\n id\n title\n rating\n price\n __typename\n }\n}",
|
|
293
|
+
variables: {},
|
|
294
|
+
prefetch: true,
|
|
295
|
+
prefetchID: "__anonymous",
|
|
296
|
+
tools: [{ name: "Top Products", description: "Shows the currently highest rated products." }],
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
resource: "index.html",
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const client = new ExtendedApolloClient({
|
|
303
|
+
manifest: manifest as ApplicationManifest,
|
|
304
|
+
});
|
|
305
|
+
await client.prefetchData();
|
|
306
|
+
|
|
307
|
+
expect(client.extract()).toMatchInlineSnapshot(`
|
|
308
|
+
{
|
|
309
|
+
"Product:1": {
|
|
310
|
+
"__typename": "Product",
|
|
311
|
+
"description": "Awesome pen",
|
|
312
|
+
"id": "1",
|
|
313
|
+
"images": [],
|
|
314
|
+
"price": 1,
|
|
315
|
+
"rating": 5,
|
|
316
|
+
"title": "Pen",
|
|
317
|
+
},
|
|
318
|
+
"Product:2": {
|
|
319
|
+
"__typename": "Product",
|
|
320
|
+
"id": "2",
|
|
321
|
+
"price": 999.99,
|
|
322
|
+
"rating": 5,
|
|
323
|
+
"title": "iPhone 17",
|
|
324
|
+
},
|
|
325
|
+
"ROOT_QUERY": {
|
|
326
|
+
"__typename": "Query",
|
|
327
|
+
"product({"id":1})": {
|
|
328
|
+
"__ref": "Product:1",
|
|
329
|
+
},
|
|
330
|
+
"topProducts": [
|
|
331
|
+
{
|
|
332
|
+
"__ref": "Product:2",
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
`);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("Should exclude extra inputs when writing to cache", async () => {
|
|
341
|
+
vi.stubGlobal("openai", {
|
|
342
|
+
toolInput: {
|
|
343
|
+
id: 1,
|
|
344
|
+
myOtherThing: 2,
|
|
345
|
+
},
|
|
346
|
+
toolOutput: {
|
|
347
|
+
result: {
|
|
348
|
+
data: {
|
|
349
|
+
product: {
|
|
350
|
+
id: "1",
|
|
351
|
+
title: "Pen",
|
|
352
|
+
rating: 5,
|
|
353
|
+
price: 1.0,
|
|
354
|
+
description: "Awesome pen",
|
|
355
|
+
images: [],
|
|
356
|
+
__typename: "Product",
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
toolResponseMetadata: {
|
|
362
|
+
toolName: "the-store--Get Product",
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const manifest = {
|
|
367
|
+
format: "apollo-ai-app-manifest",
|
|
368
|
+
version: "1",
|
|
369
|
+
name: "the-store",
|
|
370
|
+
description: "An online store selling a variety of high quality products across many different categories.",
|
|
371
|
+
hash: "f6a24922f6ad6ed8c2aa57baf3b8242ae5f38a09a6df3f2693077732434c4256",
|
|
372
|
+
operations: [
|
|
373
|
+
{
|
|
374
|
+
id: "c43af26552874026c3fb346148c5795896aa2f3a872410a0a2621cffee25291c",
|
|
375
|
+
name: "Product",
|
|
376
|
+
type: "query",
|
|
377
|
+
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
|
+
variables: { id: "ID" },
|
|
379
|
+
prefetch: false,
|
|
380
|
+
tools: [{ name: "Get Product", description: "Shows the details page for a specific product." }],
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
resource: "index.html",
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const client = new ExtendedApolloClient({
|
|
387
|
+
manifest: manifest as ApplicationManifest,
|
|
388
|
+
});
|
|
389
|
+
await client.prefetchData();
|
|
390
|
+
|
|
391
|
+
expect(client.extract()).toMatchInlineSnapshot(`
|
|
392
|
+
{
|
|
393
|
+
"Product:1": {
|
|
394
|
+
"__typename": "Product",
|
|
395
|
+
"description": "Awesome pen",
|
|
396
|
+
"id": "1",
|
|
397
|
+
"images": [],
|
|
398
|
+
"price": 1,
|
|
399
|
+
"rating": 5,
|
|
400
|
+
"title": "Pen",
|
|
401
|
+
},
|
|
402
|
+
"ROOT_QUERY": {
|
|
403
|
+
"__typename": "Query",
|
|
404
|
+
"product({"id":1})": {
|
|
405
|
+
"__ref": "Product:1",
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
}
|
|
409
|
+
`);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ApolloClient, ApolloLink, InMemoryCache } from "@apollo/client";
|
|
2
|
+
import * as Observable from "rxjs";
|
|
3
|
+
import { selectHttpOptionsAndBody } from "@apollo/client/link/http";
|
|
4
|
+
import { fallbackHttpConfig } from "@apollo/client/link/http";
|
|
5
|
+
import { DocumentTransform } from "@apollo/client";
|
|
6
|
+
import { removeDirectivesFromDocument } from "@apollo/client/utilities/internal";
|
|
7
|
+
import { parse } from "graphql";
|
|
8
|
+
import "../types/openai";
|
|
9
|
+
import { ApplicationManifest } from "../types/application-manifest";
|
|
10
|
+
|
|
11
|
+
// TODO: In the future if/when we support PQs again, do pqLink.concat(toolCallLink)
|
|
12
|
+
// Commenting this out for now.
|
|
13
|
+
// import { sha256 } from "crypto-hash";
|
|
14
|
+
// import { PersistedQueryLink } from "@apollo/client/link/persisted-queries";
|
|
15
|
+
// const pqLink = new PersistedQueryLink({
|
|
16
|
+
// sha256: (queryString) => sha256(queryString),
|
|
17
|
+
// });
|
|
18
|
+
|
|
19
|
+
// Normally, ApolloClient uses an HttpLink and sends the graphql request over HTTP
|
|
20
|
+
// In our case, we're are sending the graphql request over the "execute" tool call
|
|
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"> & {
|
|
38
|
+
link?: ApolloClient.Options["link"];
|
|
39
|
+
cache?: ApolloClient.Options["cache"];
|
|
40
|
+
manifest: ApplicationManifest;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export class ExtendedApolloClient extends ApolloClient {
|
|
44
|
+
manifest: ApplicationManifest;
|
|
45
|
+
|
|
46
|
+
constructor(options: ExtendedApolloClientOptions) {
|
|
47
|
+
super({
|
|
48
|
+
link: toolCallLink,
|
|
49
|
+
cache: options.cache ?? new InMemoryCache(),
|
|
50
|
+
// Strip out the prefetch/tool directives so they don't get sent with the operation to the server
|
|
51
|
+
documentTransform: new DocumentTransform((document) => {
|
|
52
|
+
return removeDirectivesFromDocument([{ name: "prefetch" }, { name: "tool" }], document)!;
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.manifest = options.manifest;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async prefetchData() {
|
|
60
|
+
// Write prefetched data to the cache
|
|
61
|
+
this.manifest.operations.forEach((operation) => {
|
|
62
|
+
if (operation.prefetch && operation.prefetchID && window.openai.toolOutput.prefetch[operation.prefetchID]) {
|
|
63
|
+
this.writeQuery({
|
|
64
|
+
query: parse(operation.body),
|
|
65
|
+
data: window.openai.toolOutput.prefetch[operation.prefetchID].data,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// If this operation has the tool that matches up with the tool that was executed, write the tool result to the cache
|
|
70
|
+
if (
|
|
71
|
+
operation.tools?.find(
|
|
72
|
+
(tool) => `${this.manifest.name}--${tool.name}` === window.openai.toolResponseMetadata.toolName
|
|
73
|
+
)
|
|
74
|
+
) {
|
|
75
|
+
// We need to include the variables that were used as part of the tool call so that we get a proper cache entry
|
|
76
|
+
// However, we only want to include toolInput's that were graphql operation (ignore extraInputs)
|
|
77
|
+
const variables = Object.keys(window.openai.toolInput).reduce(
|
|
78
|
+
(obj, key) => (operation.variables[key] ? { ...obj, [key]: window.openai.toolInput[key] } : obj),
|
|
79
|
+
{}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
this.writeQuery({
|
|
83
|
+
query: parse(operation.body),
|
|
84
|
+
data: window.openai.toolOutput.result.data,
|
|
85
|
+
variables,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { expect, test, vi } from "vitest";
|
|
2
|
+
import { ExtendedApolloProvider } from "./provider";
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import { ExtendedApolloClient } from "./client";
|
|
5
|
+
import { SET_GLOBALS_EVENT_TYPE } from "../types/openai";
|
|
6
|
+
|
|
7
|
+
test("Should call prefetch data when window.open is immediately available", () => {
|
|
8
|
+
vi.stubGlobal("openai", {
|
|
9
|
+
toolOutput: {},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const client = {
|
|
13
|
+
prefetchData: vi.fn(async () => {}),
|
|
14
|
+
} as unknown as ExtendedApolloClient;
|
|
15
|
+
|
|
16
|
+
render(<ExtendedApolloProvider client={client} />);
|
|
17
|
+
|
|
18
|
+
expect(client.prefetchData).toBeCalled();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("Should NOT call prefetch data when window.open is not immediately available", () => {
|
|
22
|
+
const client = {
|
|
23
|
+
prefetchData: vi.fn(async () => {}),
|
|
24
|
+
} as unknown as ExtendedApolloClient;
|
|
25
|
+
|
|
26
|
+
render(<ExtendedApolloProvider client={client} />);
|
|
27
|
+
|
|
28
|
+
expect(client.prefetchData).not.toBeCalled();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("Should call prefetch data when window.open is not immediately available and event is sent", () => {
|
|
32
|
+
const client = {
|
|
33
|
+
prefetchData: vi.fn(async () => {}),
|
|
34
|
+
} as unknown as ExtendedApolloClient;
|
|
35
|
+
|
|
36
|
+
render(<ExtendedApolloProvider client={client} />);
|
|
37
|
+
|
|
38
|
+
window.dispatchEvent(new CustomEvent(SET_GLOBALS_EVENT_TYPE));
|
|
39
|
+
|
|
40
|
+
expect(client.prefetchData).toBeCalled();
|
|
41
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { ApolloProvider } from "@apollo/client/react";
|
|
3
|
+
import { ExtendedApolloClient } from "./client";
|
|
4
|
+
import { SET_GLOBALS_EVENT_TYPE } from "../types/openai";
|
|
5
|
+
|
|
6
|
+
export const ExtendedApolloProvider = ({
|
|
7
|
+
children,
|
|
8
|
+
client,
|
|
9
|
+
}: React.PropsWithChildren<{ client: ExtendedApolloClient }>) => {
|
|
10
|
+
const [hasPreloaded, setHasPreloaded] = useState(false);
|
|
11
|
+
|
|
12
|
+
// This is to prevent against a race condition. We don't know if window.openai will be available when this loads or if it will become available shortly after.
|
|
13
|
+
// So... we create the event listener and whenever it is available, then we can process the prefetch/tool data.
|
|
14
|
+
// In practice, this should be pretty much instant
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const prefetchData = async () => {
|
|
17
|
+
await client.prefetchData();
|
|
18
|
+
setHasPreloaded(true);
|
|
19
|
+
window.removeEventListener(SET_GLOBALS_EVENT_TYPE, prefetchData);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
window.addEventListener(SET_GLOBALS_EVENT_TYPE, prefetchData, {
|
|
23
|
+
passive: true,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (window.openai?.toolOutput) {
|
|
27
|
+
window.dispatchEvent(new CustomEvent(SET_GLOBALS_EVENT_TYPE));
|
|
28
|
+
}
|
|
29
|
+
}, [setHasPreloaded]);
|
|
30
|
+
|
|
31
|
+
return hasPreloaded ? <ApolloProvider client={client}>{children}</ApolloProvider> : null;
|
|
32
|
+
};
|