@apollo/client-ai-apps 0.2.4 → 0.3.1

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.
Files changed (72) hide show
  1. package/.git-blame-ignore-revs +2 -0
  2. package/.github/workflows/compare-build-output.yml +28 -0
  3. package/.github/workflows/pr.yaml +23 -15
  4. package/.github/workflows/release.yaml +46 -46
  5. package/.prettierrc +9 -0
  6. package/config/compare-build-output-to.sh +90 -0
  7. package/dist/core/ApolloClient.d.ts +14 -0
  8. package/dist/index.d.ts +17 -10
  9. package/dist/index.js +164 -62
  10. package/dist/link/ToolCallLink.d.ts +26 -0
  11. package/dist/react/ApolloProvider.d.ts +9 -0
  12. package/dist/react/context/ToolUseContext.d.ts +15 -0
  13. package/dist/{hooks → react/hooks}/useOpenAiGlobal.d.ts +1 -1
  14. package/dist/react/hooks/useOpenExternal.d.ts +3 -0
  15. package/dist/{hooks → react/hooks}/useRequestDisplayMode.d.ts +1 -1
  16. package/dist/{hooks → react/hooks}/useToolEffect.d.ts +0 -4
  17. package/dist/react/hooks/useToolOutput.d.ts +1 -0
  18. package/dist/react/hooks/useToolResponseMetadata.d.ts +1 -0
  19. package/dist/react/hooks/useWidgetState.d.ts +4 -0
  20. package/dist/types/openai.d.ts +1 -2
  21. package/dist/vite/index.js +74 -21
  22. package/package.json +9 -2
  23. package/scripts/dev.mjs +3 -1
  24. package/src/core/ApolloClient.ts +108 -0
  25. package/src/{apollo_client/client.test.ts → core/__tests__/ApolloClient.test.ts} +232 -20
  26. package/src/index.ts +36 -10
  27. package/src/link/ToolCallLink.ts +49 -0
  28. package/src/{apollo_client/provider.tsx → react/ApolloProvider.tsx} +19 -9
  29. package/src/{apollo_client/provider.test.tsx → react/__tests__/ApolloProvider.test.tsx} +9 -9
  30. package/src/react/context/ToolUseContext.tsx +30 -0
  31. package/src/{hooks → react/hooks/__tests__}/useCallTool.test.ts +1 -1
  32. package/src/{hooks → react/hooks/__tests__}/useOpenAiGlobal.test.ts +5 -3
  33. package/src/react/hooks/__tests__/useOpenExternal.test.tsx +24 -0
  34. package/src/{hooks → react/hooks/__tests__}/useRequestDisplayMode.test.ts +2 -2
  35. package/src/{hooks → react/hooks/__tests__}/useSendFollowUpMessage.test.ts +4 -2
  36. package/src/{hooks → react/hooks/__tests__}/useToolEffect.test.tsx +27 -10
  37. package/src/{hooks → react/hooks/__tests__}/useToolInput.test.ts +1 -1
  38. package/src/{hooks → react/hooks/__tests__}/useToolName.test.ts +1 -1
  39. package/src/react/hooks/__tests__/useToolOutput.test.tsx +49 -0
  40. package/src/react/hooks/__tests__/useToolResponseMetadata.test.tsx +49 -0
  41. package/src/react/hooks/__tests__/useWidgetState.test.tsx +158 -0
  42. package/src/react/hooks/useCallTool.ts +13 -0
  43. package/src/{hooks → react/hooks}/useOpenAiGlobal.ts +11 -5
  44. package/src/react/hooks/useOpenExternal.ts +11 -0
  45. package/src/{hooks → react/hooks}/useRequestDisplayMode.ts +1 -1
  46. package/src/react/hooks/useToolEffect.tsx +37 -0
  47. package/src/{hooks → react/hooks}/useToolName.ts +1 -1
  48. package/src/react/hooks/useToolOutput.ts +5 -0
  49. package/src/react/hooks/useToolResponseMetadata.ts +5 -0
  50. package/src/react/hooks/useWidgetState.ts +48 -0
  51. package/src/testing/internal/index.ts +2 -0
  52. package/src/testing/internal/matchers/index.d.ts +9 -0
  53. package/src/testing/internal/matchers/index.ts +1 -0
  54. package/src/testing/internal/matchers/toRerender.ts +49 -0
  55. package/src/testing/internal/openai/dispatchStateChange.ts +9 -0
  56. package/src/testing/internal/openai/stubOpenAiGlobals.ts +13 -0
  57. package/src/types/openai.ts +6 -3
  58. package/src/vite/{absolute_asset_imports_plugin.test.ts → __tests__/absolute_asset_imports_plugin.test.ts} +4 -2
  59. package/src/vite/{application_manifest_plugin.test.ts → __tests__/application_manifest_plugin.test.ts} +176 -53
  60. package/src/vite/absolute_asset_imports_plugin.ts +3 -1
  61. package/src/vite/application_manifest_plugin.ts +84 -24
  62. package/vitest-setup.ts +1 -0
  63. package/dist/apollo_client/client.d.ts +0 -14
  64. package/dist/apollo_client/provider.d.ts +0 -5
  65. package/src/apollo_client/client.ts +0 -90
  66. package/src/hooks/useCallTool.ts +0 -8
  67. package/src/hooks/useToolEffect.tsx +0 -41
  68. /package/dist/{hooks → react/hooks}/useSendFollowUpMessage.d.ts +0 -0
  69. /package/dist/{hooks → react/hooks}/useToolInput.d.ts +0 -0
  70. /package/dist/{hooks → react/hooks}/useToolName.d.ts +0 -0
  71. /package/src/{hooks → react/hooks}/useSendFollowUpMessage.ts +0 -0
  72. /package/src/{hooks → react/hooks}/useToolInput.ts +0 -0
@@ -35,7 +35,9 @@ const getRawValue = (node: ValueNode): any => {
35
35
  return acc;
36
36
  }, {});
37
37
  default:
38
- throw new Error(`Error when parsing directive values: unexpected type '${node.kind}'`);
38
+ throw new Error(
39
+ `Error when parsing directive values: unexpected type '${node.kind}'`
40
+ );
39
41
  }
40
42
  };
41
43
 
@@ -48,7 +50,9 @@ const getTypedDirectiveArgument = (
48
50
  return undefined;
49
51
  }
50
52
 
51
- let argument = directiveArguments.find((directiveArgument) => directiveArgument.name.value === argumentName);
53
+ let argument = directiveArguments.find(
54
+ (directiveArgument) => directiveArgument.name.value === argumentName
55
+ );
52
56
 
53
57
  if (!argument) {
54
58
  return undefined;
@@ -80,39 +84,66 @@ export const ApplicationManifestPlugin = () => {
80
84
  const client = new ApolloClient({
81
85
  cache: clientCache,
82
86
  link: new ApolloLink((operation) => {
83
- const body = print(removeClientDirective(sortTopLevelDefinitions(operation.query)));
87
+ const body = print(
88
+ removeClientDirective(sortTopLevelDefinitions(operation.query))
89
+ );
84
90
  const name = operation.operationName;
85
91
  const variables = (
86
- operation.query.definitions.find((d) => d.kind === "OperationDefinition") as OperationDefinitionNode
92
+ operation.query.definitions.find(
93
+ (d) => d.kind === "OperationDefinition"
94
+ ) as OperationDefinitionNode
87
95
  ).variableDefinitions?.reduce(
88
- (obj, varDef) => ({ ...obj, [varDef.variable.name.value]: getTypeName(varDef.type) }),
96
+ (obj, varDef) => ({
97
+ ...obj,
98
+ [varDef.variable.name.value]: getTypeName(varDef.type),
99
+ }),
89
100
  {}
90
101
  );
91
102
  const type = (
92
- operation.query.definitions.find((d) => d.kind === "OperationDefinition") as OperationDefinitionNode
103
+ operation.query.definitions.find(
104
+ (d) => d.kind === "OperationDefinition"
105
+ ) as OperationDefinitionNode
93
106
  ).operation;
94
107
  const prefetch = (
95
- operation.query.definitions.find((d) => d.kind === "OperationDefinition") as OperationDefinitionNode
108
+ operation.query.definitions.find(
109
+ (d) => d.kind === "OperationDefinition"
110
+ ) as OperationDefinitionNode
96
111
  ).directives?.some((d) => d.name.value === "prefetch");
97
112
  const id = createHash("sha256").update(body).digest("hex");
98
113
  // TODO: For now, you can only have 1 operation marked as prefetch. In the future, we'll likely support more than 1, and the "prefetchId" will be defined on the `@prefetch` itself as an argument
99
114
  const prefetchID = prefetch ? "__anonymous" : undefined;
100
115
 
101
116
  const tools = (
102
- operation.query.definitions.find((d) => d.kind === "OperationDefinition") as OperationDefinitionNode
117
+ operation.query.definitions.find(
118
+ (d) => d.kind === "OperationDefinition"
119
+ ) as OperationDefinitionNode
103
120
  ).directives
104
121
  ?.filter((d) => d.name.value === "tool")
105
122
  .map((directive) => {
106
- const name = getTypedDirectiveArgument("name", Kind.STRING, directive.arguments);
107
- const description = getTypedDirectiveArgument("description", Kind.STRING, directive.arguments);
108
- const extraInputs = getTypedDirectiveArgument("extraInputs", Kind.LIST, directive.arguments);
123
+ const name = getTypedDirectiveArgument(
124
+ "name",
125
+ Kind.STRING,
126
+ directive.arguments
127
+ );
128
+ const description = getTypedDirectiveArgument(
129
+ "description",
130
+ Kind.STRING,
131
+ directive.arguments
132
+ );
133
+ const extraInputs = getTypedDirectiveArgument(
134
+ "extraInputs",
135
+ Kind.LIST,
136
+ directive.arguments
137
+ );
109
138
 
110
139
  if (!name) {
111
140
  throw new Error("'name' argument must be supplied for @tool");
112
141
  }
113
142
 
114
143
  if (!description) {
115
- throw new Error("'description' argument must be supplied for @tool");
144
+ throw new Error(
145
+ "'description' argument must be supplied for @tool"
146
+ );
116
147
  }
117
148
 
118
149
  return {
@@ -122,7 +153,9 @@ export const ApplicationManifestPlugin = () => {
122
153
  };
123
154
  });
124
155
 
125
- return Observable.of({ data: { id, name, type, body, variables, prefetch, prefetchID, tools } });
156
+ return Observable.of({
157
+ data: { id, name, type, body, variables, prefetch, prefetchID, tools },
158
+ });
126
159
  }),
127
160
  });
128
161
 
@@ -146,16 +179,27 @@ export const ApplicationManifestPlugin = () => {
146
179
 
147
180
  const operations = [];
148
181
  for (const source of sources) {
149
- const type = (source.node.definitions.find((d) => d.kind === "OperationDefinition") as OperationDefinitionNode)
150
- .operation;
182
+ const type = (
183
+ source.node.definitions.find(
184
+ (d) => d.kind === "OperationDefinition"
185
+ ) as OperationDefinitionNode
186
+ ).operation;
151
187
 
152
188
  let result;
153
189
  if (type === "query") {
154
- result = await client.query({ query: source.node, fetchPolicy: "no-cache" });
190
+ result = await client.query({
191
+ query: source.node,
192
+ fetchPolicy: "no-cache",
193
+ });
155
194
  } else if (type === "mutation") {
156
- result = await client.mutate({ mutation: source.node, fetchPolicy: "no-cache" });
195
+ result = await client.mutate({
196
+ mutation: source.node,
197
+ fetchPolicy: "no-cache",
198
+ });
157
199
  } else {
158
- throw new Error("Found an unsupported operation type. Only Query and Mutation are supported.");
200
+ throw new Error(
201
+ "Found an unsupported operation type. Only Query and Mutation are supported."
202
+ );
159
203
  }
160
204
  operations.push(result.data);
161
205
  }
@@ -168,7 +212,9 @@ export const ApplicationManifestPlugin = () => {
168
212
  };
169
213
 
170
214
  const generateManifest = async () => {
171
- const operations = Array.from(cache.values()).flatMap((entry) => entry.operations);
215
+ const operations = Array.from(cache.values()).flatMap(
216
+ (entry) => entry.operations
217
+ );
172
218
  if (operations.filter((o) => o.prefetch).length > 1) {
173
219
  throw new Error(
174
220
  "Found multiple operations marked as `@prefetch`. You can only mark 1 operation with `@prefetch`."
@@ -199,7 +245,9 @@ export const ApplicationManifestPlugin = () => {
199
245
  name: packageJson.name,
200
246
  description: packageJson.description,
201
247
  hash: createHash("sha256").update(Date.now().toString()).digest("hex"),
202
- operations: Array.from(cache.values()).flatMap((entry) => entry.operations),
248
+ operations: Array.from(cache.values()).flatMap(
249
+ (entry) => entry.operations
250
+ ),
203
251
  resource,
204
252
  csp: {
205
253
  connectDomains: packageJson.csp?.connectDomains ?? [],
@@ -208,7 +256,11 @@ export const ApplicationManifestPlugin = () => {
208
256
  };
209
257
 
210
258
  // Always write to build directory so the MCP server picks it up
211
- const dest = path.resolve(root, config.build.outDir, ".application-manifest.json");
259
+ const dest = path.resolve(
260
+ root,
261
+ config.build.outDir,
262
+ ".application-manifest.json"
263
+ );
212
264
  mkdirSync(path.dirname(dest), { recursive: true });
213
265
  writeFileSync(dest, JSON.stringify(manifest));
214
266
 
@@ -279,8 +331,14 @@ export function sortTopLevelDefinitions(query: DocumentNode): DocumentNode {
279
331
  // non-executable definitions don't have to have names (even though any
280
332
  // DocumentNode actually passed here should only have executable
281
333
  // definitions).
282
- const aName = a.kind === "OperationDefinition" || a.kind === "FragmentDefinition" ? a.name?.value ?? "" : "";
283
- const bName = b.kind === "OperationDefinition" || b.kind === "FragmentDefinition" ? b.name?.value ?? "" : "";
334
+ const aName =
335
+ a.kind === "OperationDefinition" || a.kind === "FragmentDefinition" ?
336
+ (a.name?.value ?? "")
337
+ : "";
338
+ const bName =
339
+ b.kind === "OperationDefinition" || b.kind === "FragmentDefinition" ?
340
+ (b.name?.value ?? "")
341
+ : "";
284
342
 
285
343
  // Sort by name ascending.
286
344
  if (aName < bName) {
@@ -306,7 +364,9 @@ function removeClientDirective(doc: DocumentNode) {
306
364
  OperationDefinition(node) {
307
365
  return {
308
366
  ...node,
309
- directives: node.directives?.filter((d) => d.name.value !== "prefetch" && d.name.value !== "tool"),
367
+ directives: node.directives?.filter(
368
+ (d) => d.name.value !== "prefetch" && d.name.value !== "tool"
369
+ ),
310
370
  };
311
371
  },
312
372
  });
package/vitest-setup.ts CHANGED
@@ -1 +1,2 @@
1
1
  import "@testing-library/jest-dom/vitest";
2
+ import "./src/testing/internal/matchers";
@@ -1,14 +0,0 @@
1
- import { ApolloClient } from "@apollo/client";
2
- import "../types/openai";
3
- import { ApplicationManifest } from "../types/application-manifest";
4
- type ExtendedApolloClientOptions = Omit<ApolloClient.Options, "link" | "cache"> & {
5
- link?: ApolloClient.Options["link"];
6
- cache?: ApolloClient.Options["cache"];
7
- manifest: ApplicationManifest;
8
- };
9
- export declare class ExtendedApolloClient extends ApolloClient {
10
- manifest: ApplicationManifest;
11
- constructor(options: ExtendedApolloClientOptions);
12
- prefetchData(): Promise<void>;
13
- }
14
- export {};
@@ -1,5 +0,0 @@
1
- import React from "react";
2
- import { ExtendedApolloClient } from "./client";
3
- export declare const ExtendedApolloProvider: ({ children, client, }: React.PropsWithChildren<{
4
- client: ExtendedApolloClient;
5
- }>) => React.JSX.Element;
@@ -1,90 +0,0 @@
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
- }
@@ -1,8 +0,0 @@
1
- type UseCallToolResult = <K>(toolId: string, variables?: Record<string, unknown> | undefined) => Promise<K>;
2
-
3
- export const useCallTool = (): UseCallToolResult => {
4
- const callTool = async (toolId: string, variables: Record<string, unknown> | undefined = {}) =>
5
- await window.openai?.callTool(toolId, variables);
6
-
7
- return callTool;
8
- };
@@ -1,41 +0,0 @@
1
- import React, { useEffect, useState } from "react";
2
- import { useToolName } from "./useToolName";
3
- import { useToolInput } from "./useToolInput";
4
-
5
- type ToolUseState = {
6
- appName: string;
7
- hasNavigated: boolean;
8
- setHasNavigated: (v: boolean) => void;
9
- };
10
-
11
- const ToolUseContext = React.createContext<ToolUseState | null>(null);
12
-
13
- export function ToolUseProvider({ children, appName }: { children: any; appName: string }) {
14
- const [hasNavigated, setHasNavigated] = useState(false);
15
-
16
- return (
17
- <ToolUseContext.Provider value={{ hasNavigated, setHasNavigated, appName }}>{children}</ToolUseContext.Provider>
18
- );
19
- }
20
-
21
- export const useToolEffect = (
22
- toolName: string | string[],
23
- effect: (toolInput: any) => void,
24
- deps: React.DependencyList = []
25
- ) => {
26
- const ctx = React.useContext(ToolUseContext);
27
- const fullToolName = useToolName();
28
- const toolInput = useToolInput();
29
- if (!ctx) throw new Error("useToolEffect must be used within ToolUseProvider");
30
-
31
- const toolNames = Array.isArray(toolName) ? toolName : [toolName];
32
-
33
- useEffect(() => {
34
- const matches = toolNames.some((name) => fullToolName === `${ctx.appName}--${name}`);
35
-
36
- if (!ctx.hasNavigated && matches) {
37
- effect(toolInput);
38
- ctx.setHasNavigated(true);
39
- }
40
- }, [ctx.hasNavigated, ctx.setHasNavigated, ctx.appName, toolNames, fullToolName, toolInput, ...deps]);
41
- };
File without changes
File without changes
File without changes