Skip to content

Commit 4405cd3

Browse files
committed
feat(ai): add files for AI mocking
1 parent b407ae4 commit 4405cd3

File tree

5 files changed

+221
-0
lines changed

5 files changed

+221
-0
lines changed

packages/ai/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./mocking/AIAdapter.js";
2+
export * from "./mocking/AIMockLink.js";
3+
export * from "./mocking/AIMockProvider.js";
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { ApolloLink } from "@apollo/client";
2+
import { print } from "graphql";
3+
import { BASE_SYSTEM_PROMPT } from "./consts.js";
4+
5+
export declare namespace AIAdapter {
6+
export interface Options {
7+
systemPrompt?: string;
8+
}
9+
10+
export type Result = ApolloLink.Result;
11+
}
12+
13+
export abstract class AIAdapter {
14+
public providedSystemPrompt: string | undefined;
15+
protected systemPrompt: string;
16+
17+
constructor(options: AIAdapter.Options = {}) {
18+
this.systemPrompt = BASE_SYSTEM_PROMPT;
19+
if (options.systemPrompt) {
20+
this.providedSystemPrompt = options.systemPrompt;
21+
this.systemPrompt += `\n\n${this.providedSystemPrompt}`;
22+
}
23+
}
24+
25+
public generateResponseForOperation(
26+
operation: ApolloLink.Operation,
27+
prompt: string
28+
): Promise<AIAdapter.Result> {
29+
return Promise.resolve({ data: null });
30+
}
31+
32+
protected createPrompt(
33+
{ query, variables }: ApolloLink.Operation,
34+
prompt: string
35+
): string {
36+
// Try to get the GraphQL query document string
37+
// from the AST location if available, otherwise
38+
// use the `print` function to get the query string.
39+
//
40+
// The AST location may not be available if the query
41+
// was parsed with the `noLocation: true` option.
42+
//
43+
// If the query document string is available through
44+
// the AST location, it will save some processing time
45+
// over the `print` function.
46+
const queryString = query?.loc?.source?.body ?? print(query);
47+
48+
let promptVariables = "";
49+
if (variables) {
50+
promptVariables = `
51+
52+
With variables:
53+
\`\`\`json
54+
${JSON.stringify(variables, null, 2)}
55+
\`\`\``;
56+
}
57+
58+
return `Give me mock data that fulfills this query:
59+
\`\`\`graphql
60+
${queryString}
61+
\`\`\`
62+
${promptVariables}
63+
${prompt ? `\nAdditional instructions:\n${prompt}` : ""}
64+
`;
65+
}
66+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { ApolloLink, Observable } from "@apollo/client";
2+
import { AIAdapter } from "./AIAdapter.js";
3+
4+
export declare namespace AIMockLink {
5+
export type DefaultOptions = {};
6+
7+
export interface Options {
8+
systemPrompt?: string;
9+
showWarnings?: boolean;
10+
defaultOptions?: DefaultOptions;
11+
}
12+
}
13+
14+
export class AIMockLink extends ApolloLink {
15+
public showWarnings: boolean = true;
16+
public systemPrompt?: string = "";
17+
18+
public static defaultOptions: AIMockLink.DefaultOptions = {};
19+
20+
constructor(
21+
private adapter: AIAdapter,
22+
options: AIMockLink.Options = {}
23+
) {
24+
super();
25+
26+
this.systemPrompt = options.systemPrompt;
27+
this.showWarnings = options.showWarnings ?? true;
28+
}
29+
30+
public request(
31+
operation: ApolloLink.Operation
32+
): Observable<ApolloLink.Result> {
33+
const prompt = operation.getContext().prompt;
34+
35+
return new Observable((observer) => {
36+
try {
37+
this.adapter
38+
.generateResponseForOperation(operation, prompt)
39+
.then((result) => {
40+
// Notify the observer with the generated response
41+
observer.next(result);
42+
observer.complete();
43+
});
44+
} catch (error) {
45+
observer.error(error);
46+
observer.complete();
47+
}
48+
});
49+
}
50+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import * as React from "react";
2+
3+
import { ApolloClient } from "@apollo/client";
4+
import type { ApolloCache } from "@apollo/client/cache";
5+
import { InMemoryCache as Cache } from "@apollo/client/cache";
6+
import type { ApolloLink } from "@apollo/client/link";
7+
import type { LocalState } from "@apollo/client/local-state";
8+
import { ApolloProvider } from "@apollo/client/react";
9+
import { MockLink } from "@apollo/client/testing";
10+
import { AIAdapter } from "./AIAdapter.js";
11+
import { AIMockLink } from "./AIMockLink.js";
12+
13+
export interface AIMockedProviderProps {
14+
adapter: AIAdapter;
15+
systemPrompt?: string;
16+
defaultOptions?: ApolloClient.DefaultOptions;
17+
cache?: ApolloCache;
18+
localState?: LocalState;
19+
childProps?: object;
20+
children?: any;
21+
link?: ApolloLink;
22+
showWarnings?: boolean;
23+
mockLinkDefaultOptions?: MockLink.DefaultOptions;
24+
devtools?: ApolloClient.Options["devtools"];
25+
}
26+
27+
interface AIMockedProviderState {
28+
client: ApolloClient;
29+
}
30+
31+
export class AIMockedProvider extends React.Component<
32+
AIMockedProviderProps,
33+
AIMockedProviderState
34+
> {
35+
constructor(props: AIMockedProviderProps) {
36+
super(props);
37+
38+
const {
39+
adapter,
40+
systemPrompt,
41+
defaultOptions,
42+
cache,
43+
localState,
44+
link,
45+
showWarnings,
46+
mockLinkDefaultOptions,
47+
devtools,
48+
} = this.props;
49+
50+
const client = new ApolloClient({
51+
cache: cache || new Cache(),
52+
defaultOptions,
53+
link:
54+
link ||
55+
new AIMockLink(adapter, {
56+
systemPrompt,
57+
showWarnings,
58+
defaultOptions: mockLinkDefaultOptions,
59+
}),
60+
localState,
61+
devtools,
62+
});
63+
64+
this.state = {
65+
client,
66+
};
67+
}
68+
69+
public render() {
70+
const { children, childProps } = this.props;
71+
const { client } = this.state;
72+
73+
return React.isValidElement(children) ?
74+
<ApolloProvider client={client}>
75+
{React.cloneElement(React.Children.only(children), { ...childProps })}
76+
</ApolloProvider>
77+
: null;
78+
}
79+
80+
public componentWillUnmount() {
81+
// Since this.state.client was created in the
82+
// constructor, it's this MockedProvider's responsibility
83+
// to terminate it.
84+
this.state.client.stop();
85+
}
86+
}

packages/ai/src/mocking/consts.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const BASE_SYSTEM_PROMPT = `
2+
You are returning mock data for a GraphQL API.
3+
4+
When generating image URLs, use these reliable placeholder services with unique identifiers:
5+
- https://picsum.photos/[width]/[height]?random=[unique_identifier] (e.g., https://picsum.photos/400/300?random=asdf, ?random=ytal, etc.)
6+
- https://via.placeholder.com/[width]x[height]/[color]/[text_color]?text=[context] (e.g., ?text=Product+asdf)
7+
- https://placehold.co/[width]x[height]/[color]/[text_color]?text=[context] (e.g, ?text=User+Avatar)
8+
9+
For list items, increment the random number or use contextual text to ensure unique images.
10+
11+
Avoid using numbers for unique identifiers. Unique identifier and typename combinations should result in consistent data.
12+
13+
For example, say something is named "Foobar", you should use a unique identifier like "foobar" and not a number.
14+
15+
Remember context and data based on the unique identifier and typename so that data is consistent.
16+
`;

0 commit comments

Comments
 (0)