@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
@@ -0,0 +1,26 @@
1
+ import { ApolloLink, Observable } from "@apollo/client";
2
+ /**
3
+ * A terminating link that sends a GraphQL request through an agent tool call.
4
+ * When providing a custom link chain to `ApolloClient`, `ApolloClient` will
5
+ * validate that the terminating link is an instance of this link.
6
+ *
7
+ * @example Provding a custom link chain
8
+ *
9
+ * ```ts
10
+ * import { ApolloLink } from "@apollo/client";
11
+ * import { ApolloClient, ToolCallLink } from "@apollo/client-ai-apps";
12
+ *
13
+ * const link = ApolloLink.from([
14
+ * ...otherLinks,
15
+ * new ToolCallLink()
16
+ * ]);
17
+ *
18
+ * const client = new ApolloClient({
19
+ * link,
20
+ * // ...
21
+ * });
22
+ * ```
23
+ */
24
+ export declare class ToolCallLink extends ApolloLink {
25
+ request(operation: ApolloLink.Operation): Observable<ApolloLink.Result>;
26
+ }
@@ -0,0 +1,9 @@
1
+ import React, { ReactNode } from "react";
2
+ import { ApolloClient } from "../core/ApolloClient";
3
+ export declare namespace ApolloProvider {
4
+ interface Props {
5
+ children?: ReactNode;
6
+ client: ApolloClient;
7
+ }
8
+ }
9
+ export declare const ApolloProvider: ({ children, client }: ApolloProvider.Props) => React.JSX.Element;
@@ -0,0 +1,15 @@
1
+ import React, { ReactNode } from "react";
2
+ interface ToolUseState {
3
+ appName: string;
4
+ hasNavigated: boolean;
5
+ setHasNavigated: (v: boolean) => void;
6
+ }
7
+ export declare namespace ToolUseProvider {
8
+ interface Props {
9
+ children?: ReactNode;
10
+ appName: string;
11
+ }
12
+ }
13
+ export declare function ToolUseProvider({ children, appName }: ToolUseProvider.Props): React.JSX.Element;
14
+ export declare function useToolUseState(): ToolUseState;
15
+ export {};
@@ -1,2 +1,2 @@
1
- import { OpenAiGlobals } from "../types/openai";
1
+ import { OpenAiGlobals } from "../../types/openai";
2
2
  export declare function useOpenAiGlobal<K extends keyof OpenAiGlobals>(key: K): OpenAiGlobals[K];
@@ -0,0 +1,3 @@
1
+ export declare function useOpenExternal(): (payload: {
2
+ href: string;
3
+ }) => void;
@@ -1,4 +1,4 @@
1
- import { DisplayMode } from "../types/openai";
1
+ import { DisplayMode } from "../../types/openai";
2
2
  export declare const useRequestDisplayMode: () => (args: {
3
3
  mode: DisplayMode;
4
4
  }) => Promise<{
@@ -1,6 +1,2 @@
1
1
  import React from "react";
2
- export declare function ToolUseProvider({ children, appName }: {
3
- children: any;
4
- appName: string;
5
- }): React.JSX.Element;
6
2
  export declare const useToolEffect: (toolName: string | string[], effect: (toolInput: any) => void, deps?: React.DependencyList) => void;
@@ -0,0 +1 @@
1
+ export declare function useToolOutput(): import("../..").UnknownObject;
@@ -0,0 +1 @@
1
+ export declare function useToolResponseMetadata(): import("../..").UnknownObject;
@@ -0,0 +1,4 @@
1
+ import { SetStateAction } from "react";
2
+ import { UnknownObject } from "../../types/openai";
3
+ export declare function useWidgetState<T extends UnknownObject>(defaultState: T | (() => T)): readonly [T, (state: SetStateAction<T>) => void];
4
+ export declare function useWidgetState<T extends UnknownObject>(defaultState?: T | (() => T | null) | null): readonly [T | null, (state: SetStateAction<T | null>) => void];
@@ -1,4 +1,4 @@
1
- type UnknownObject = any;
1
+ export type UnknownObject = Record<string, unknown>;
2
2
  declare global {
3
3
  interface Window {
4
4
  openai: API<any> & OpenAiGlobals;
@@ -70,4 +70,3 @@ export type UserAgent = {
70
70
  touch: boolean;
71
71
  };
72
72
  };
73
- export {};
@@ -26,14 +26,18 @@ var getRawValue = (node) => {
26
26
  return acc;
27
27
  }, {});
28
28
  default:
29
- throw new Error(`Error when parsing directive values: unexpected type '${node.kind}'`);
29
+ throw new Error(
30
+ `Error when parsing directive values: unexpected type '${node.kind}'`
31
+ );
30
32
  }
31
33
  };
32
34
  var getTypedDirectiveArgument = (argumentName, expectedType, directiveArguments) => {
33
35
  if (!directiveArguments || directiveArguments.length === 0) {
34
36
  return void 0;
35
37
  }
36
- let argument = directiveArguments.find((directiveArgument) => directiveArgument.name.value === argumentName);
38
+ let argument = directiveArguments.find(
39
+ (directiveArgument) => directiveArgument.name.value === argumentName
40
+ );
37
41
  if (!argument) {
38
42
  return void 0;
39
43
  }
@@ -59,25 +63,52 @@ var ApplicationManifestPlugin = () => {
59
63
  const client = new ApolloClient({
60
64
  cache: clientCache,
61
65
  link: new ApolloLink((operation) => {
62
- const body = print(removeClientDirective(sortTopLevelDefinitions(operation.query)));
66
+ const body = print(
67
+ removeClientDirective(sortTopLevelDefinitions(operation.query))
68
+ );
63
69
  const name = operation.operationName;
64
- const variables = operation.query.definitions.find((d) => d.kind === "OperationDefinition").variableDefinitions?.reduce(
65
- (obj, varDef) => ({ ...obj, [varDef.variable.name.value]: getTypeName(varDef.type) }),
70
+ const variables = operation.query.definitions.find(
71
+ (d) => d.kind === "OperationDefinition"
72
+ ).variableDefinitions?.reduce(
73
+ (obj, varDef) => ({
74
+ ...obj,
75
+ [varDef.variable.name.value]: getTypeName(varDef.type)
76
+ }),
66
77
  {}
67
78
  );
68
- const type = operation.query.definitions.find((d) => d.kind === "OperationDefinition").operation;
69
- const prefetch = operation.query.definitions.find((d) => d.kind === "OperationDefinition").directives?.some((d) => d.name.value === "prefetch");
79
+ const type = operation.query.definitions.find(
80
+ (d) => d.kind === "OperationDefinition"
81
+ ).operation;
82
+ const prefetch = operation.query.definitions.find(
83
+ (d) => d.kind === "OperationDefinition"
84
+ ).directives?.some((d) => d.name.value === "prefetch");
70
85
  const id = createHash("sha256").update(body).digest("hex");
71
86
  const prefetchID = prefetch ? "__anonymous" : void 0;
72
- const tools = operation.query.definitions.find((d) => d.kind === "OperationDefinition").directives?.filter((d) => d.name.value === "tool").map((directive) => {
73
- const name2 = getTypedDirectiveArgument("name", Kind.STRING, directive.arguments);
74
- const description = getTypedDirectiveArgument("description", Kind.STRING, directive.arguments);
75
- const extraInputs = getTypedDirectiveArgument("extraInputs", Kind.LIST, directive.arguments);
87
+ const tools = operation.query.definitions.find(
88
+ (d) => d.kind === "OperationDefinition"
89
+ ).directives?.filter((d) => d.name.value === "tool").map((directive) => {
90
+ const name2 = getTypedDirectiveArgument(
91
+ "name",
92
+ Kind.STRING,
93
+ directive.arguments
94
+ );
95
+ const description = getTypedDirectiveArgument(
96
+ "description",
97
+ Kind.STRING,
98
+ directive.arguments
99
+ );
100
+ const extraInputs = getTypedDirectiveArgument(
101
+ "extraInputs",
102
+ Kind.LIST,
103
+ directive.arguments
104
+ );
76
105
  if (!name2) {
77
106
  throw new Error("'name' argument must be supplied for @tool");
78
107
  }
79
108
  if (!description) {
80
- throw new Error("'description' argument must be supplied for @tool");
109
+ throw new Error(
110
+ "'description' argument must be supplied for @tool"
111
+ );
81
112
  }
82
113
  return {
83
114
  name: name2,
@@ -85,7 +116,9 @@ var ApplicationManifestPlugin = () => {
85
116
  extraInputs
86
117
  };
87
118
  });
88
- return Observable.of({ data: { id, name, type, body, variables, prefetch, prefetchID, tools } });
119
+ return Observable.of({
120
+ data: { id, name, type, body, variables, prefetch, prefetchID, tools }
121
+ });
89
122
  })
90
123
  });
91
124
  const processFile = async (file) => {
@@ -105,14 +138,24 @@ var ApplicationManifestPlugin = () => {
105
138
  }));
106
139
  const operations = [];
107
140
  for (const source of sources) {
108
- const type = source.node.definitions.find((d) => d.kind === "OperationDefinition").operation;
141
+ const type = source.node.definitions.find(
142
+ (d) => d.kind === "OperationDefinition"
143
+ ).operation;
109
144
  let result;
110
145
  if (type === "query") {
111
- result = await client.query({ query: source.node, fetchPolicy: "no-cache" });
146
+ result = await client.query({
147
+ query: source.node,
148
+ fetchPolicy: "no-cache"
149
+ });
112
150
  } else if (type === "mutation") {
113
- result = await client.mutate({ mutation: source.node, fetchPolicy: "no-cache" });
151
+ result = await client.mutate({
152
+ mutation: source.node,
153
+ fetchPolicy: "no-cache"
154
+ });
114
155
  } else {
115
- throw new Error("Found an unsupported operation type. Only Query and Mutation are supported.");
156
+ throw new Error(
157
+ "Found an unsupported operation type. Only Query and Mutation are supported."
158
+ );
116
159
  }
117
160
  operations.push(result.data);
118
161
  }
@@ -123,7 +166,9 @@ var ApplicationManifestPlugin = () => {
123
166
  });
124
167
  };
125
168
  const generateManifest = async () => {
126
- const operations = Array.from(cache.values()).flatMap((entry) => entry.operations);
169
+ const operations = Array.from(cache.values()).flatMap(
170
+ (entry) => entry.operations
171
+ );
127
172
  if (operations.filter((o) => o.prefetch).length > 1) {
128
173
  throw new Error(
129
174
  "Found multiple operations marked as `@prefetch`. You can only mark 1 operation with `@prefetch`."
@@ -150,14 +195,20 @@ var ApplicationManifestPlugin = () => {
150
195
  name: packageJson.name,
151
196
  description: packageJson.description,
152
197
  hash: createHash("sha256").update(Date.now().toString()).digest("hex"),
153
- operations: Array.from(cache.values()).flatMap((entry) => entry.operations),
198
+ operations: Array.from(cache.values()).flatMap(
199
+ (entry) => entry.operations
200
+ ),
154
201
  resource,
155
202
  csp: {
156
203
  connectDomains: packageJson.csp?.connectDomains ?? [],
157
204
  resourceDomains: packageJson.csp?.resourceDomains ?? []
158
205
  }
159
206
  };
160
- const dest = path.resolve(root, config.build.outDir, ".application-manifest.json");
207
+ const dest = path.resolve(
208
+ root,
209
+ config.build.outDir,
210
+ ".application-manifest.json"
211
+ );
161
212
  mkdirSync(path.dirname(dest), { recursive: true });
162
213
  writeFileSync(dest, JSON.stringify(manifest));
163
214
  writeFileSync(".application-manifest.json", JSON.stringify(manifest));
@@ -224,7 +275,9 @@ function removeClientDirective(doc) {
224
275
  OperationDefinition(node) {
225
276
  return {
226
277
  ...node,
227
- directives: node.directives?.filter((d) => d.name.value !== "prefetch" && d.name.value !== "tool")
278
+ directives: node.directives?.filter(
279
+ (d) => d.name.value !== "prefetch" && d.name.value !== "tool"
280
+ )
228
281
  };
229
282
  }
230
283
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apollo/client-ai-apps",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -17,12 +17,16 @@
17
17
  },
18
18
  "scripts": {
19
19
  "dev": "node ./scripts/dev.mjs",
20
+ "prebuild": "npm run clean",
20
21
  "build": "npm run build:react && npm run build:vite",
21
22
  "build:react": "node ./scripts/build.mjs && tsc src/index.ts --emitDeclarationOnly --declaration --outDir dist --skipLibCheck --lib ES2015,DOM --target ES2015 --moduleResolution bundler --jsx react",
22
23
  "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",
24
+ "clean": "rimraf dist",
23
25
  "test": "vitest run --coverage",
24
26
  "test:watch": "vitest --coverage",
25
- "changeset": "knope document-change"
27
+ "changeset": "knope document-change",
28
+ "format": "prettier --write .",
29
+ "format:check": "prettier --check ."
26
30
  },
27
31
  "keywords": [],
28
32
  "author": "",
@@ -31,6 +35,7 @@
31
35
  "devDependencies": {
32
36
  "@testing-library/jest-dom": "^6.9.1",
33
37
  "@testing-library/react": "^16.3.0",
38
+ "@testing-library/react-render-stream": "^2.0.2",
34
39
  "@types/node": "^24.10.0",
35
40
  "@types/react": "^19.2.2",
36
41
  "@vitejs/plugin-react": "^5.1.1",
@@ -38,6 +43,8 @@
38
43
  "esbuild": "^0.25.12",
39
44
  "graphql": "^16.9.0",
40
45
  "happy-dom": "^20.0.10",
46
+ "prettier": "^3.7.4",
47
+ "rimraf": "^6.1.2",
41
48
  "rxjs": "^7.8.1",
42
49
  "typescript": "^5.9.3",
43
50
  "vitest": "^4.0.13"
package/scripts/dev.mjs CHANGED
@@ -10,7 +10,9 @@ let ctx = await esbuild.context({
10
10
  name: "rebuild-logger",
11
11
  setup(build) {
12
12
  build.onEnd((result) => {
13
- console.log(`Rebuilt at ${new Date().toLocaleTimeString()} (${result.errors.length} errors)`);
13
+ console.log(
14
+ `Rebuilt at ${new Date().toLocaleTimeString()} (${result.errors.length} errors)`
15
+ );
14
16
  });
15
17
  },
16
18
  },
@@ -0,0 +1,108 @@
1
+ import type { ApolloLink } from "@apollo/client";
2
+ import { ApolloClient as BaseApolloClient } from "@apollo/client";
3
+ import { DocumentTransform } from "@apollo/client";
4
+ import { removeDirectivesFromDocument } from "@apollo/client/utilities/internal";
5
+ import { parse } from "graphql";
6
+ import { __DEV__ } from "@apollo/client/utilities/environment";
7
+ import "../types/openai";
8
+ import { ApplicationManifest } from "../types/application-manifest";
9
+ import { ToolCallLink } from "../link/ToolCallLink";
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
+ export declare namespace ApolloClient {
20
+ // This allows us to extend the options with the "manifest" option AND make link optional (it is normally required)
21
+ export interface Options extends Omit<BaseApolloClient.Options, "link"> {
22
+ link?: BaseApolloClient.Options["link"];
23
+ manifest: ApplicationManifest;
24
+ }
25
+ }
26
+
27
+ export class ApolloClient extends BaseApolloClient {
28
+ manifest: ApplicationManifest;
29
+
30
+ constructor(options: ApolloClient.Options) {
31
+ const link = options.link ?? new ToolCallLink();
32
+
33
+ if (__DEV__) {
34
+ validateTerminatingLink(link);
35
+ }
36
+
37
+ super({
38
+ ...options,
39
+ link,
40
+ // Strip out the prefetch/tool directives so they don't get sent with the operation to the server
41
+ documentTransform: new DocumentTransform((document) => {
42
+ return removeDirectivesFromDocument(
43
+ [{ name: "prefetch" }, { name: "tool" }],
44
+ document
45
+ )!;
46
+ }),
47
+ });
48
+
49
+ this.manifest = options.manifest;
50
+ }
51
+
52
+ async prefetchData() {
53
+ // Write prefetched data to the cache
54
+ this.manifest.operations.forEach((operation) => {
55
+ if (
56
+ operation.prefetch &&
57
+ operation.prefetchID &&
58
+ window.openai.toolOutput?.prefetch?.[operation.prefetchID]
59
+ ) {
60
+ this.writeQuery({
61
+ query: parse(operation.body),
62
+ data: window.openai.toolOutput.prefetch[operation.prefetchID].data,
63
+ });
64
+ }
65
+
66
+ // If this operation has the tool that matches up with the tool that was executed, write the tool result to the cache
67
+ if (
68
+ operation.tools?.find(
69
+ (tool) =>
70
+ `${this.manifest.name}--${tool.name}` ===
71
+ window.openai.toolResponseMetadata?.toolName
72
+ )
73
+ ) {
74
+ // We need to include the variables that were used as part of the tool call so that we get a proper cache entry
75
+ // However, we only want to include toolInput's that were graphql operation (ignore extraInputs)
76
+ const variables = Object.keys(window.openai.toolInput).reduce(
77
+ (obj, key) =>
78
+ operation.variables?.[key] ?
79
+ { ...obj, [key]: window.openai.toolInput[key] }
80
+ : obj,
81
+ {}
82
+ );
83
+
84
+ if (window.openai.toolOutput) {
85
+ this.writeQuery({
86
+ query: parse(operation.body),
87
+ data: (window.openai.toolOutput.result as any).data,
88
+ variables,
89
+ });
90
+ }
91
+ }
92
+ });
93
+ }
94
+ }
95
+
96
+ function validateTerminatingLink(link: ApolloLink) {
97
+ let terminatingLink = link;
98
+
99
+ while (terminatingLink.right) {
100
+ terminatingLink = terminatingLink.right;
101
+ }
102
+
103
+ if (terminatingLink.constructor.name !== "ToolCallLink") {
104
+ throw new Error(
105
+ "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."
106
+ );
107
+ }
108
+ }