Skip to content

Commit 0ba7d5b

Browse files
committed
feat: implement Pattern provider for AI SDK
1 parent ac33e3f commit 0ba7d5b

10 files changed

Lines changed: 536 additions & 84 deletions

File tree

app/(chat)/adapter.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { Ok, Err, type Result } from 'ts-results-es';
2+
3+
import { extractErrorMessageOrDefault } from '@/lib/utils';
4+
5+
import type {
6+
ApiCreateConversationResponse,
7+
ApiGetConversationResponse,
8+
ApiSendMessageResponse,
9+
ApiSendMessageStreamedResponse,
10+
} from './types';
11+
12+
const patternCoreEndpoint = process.env.PATTERN_CORE_ENDPOINT;
13+
if (!patternCoreEndpoint) {
14+
throw new Error('PATTERN_CORE_ENDPOINT is not set');
15+
}
16+
17+
/**
18+
* Get a conversation
19+
* @param accessToken
20+
* @param projectId
21+
* @param conversationId
22+
* @returns result containing the conversation
23+
*/
24+
export const getConversation = async (
25+
accessToken: string,
26+
projectId: string,
27+
conversationId: string,
28+
): Promise<Result<ApiGetConversationResponse, string>> => {
29+
try {
30+
const conversationResponse = await fetch(
31+
`${patternCoreEndpoint}/playground/conversation/${projectId}/${conversationId}`,
32+
{
33+
headers: {
34+
Authorization: `Bearer ${accessToken}`,
35+
'Content-Type': 'application/json',
36+
},
37+
},
38+
);
39+
40+
if (conversationResponse.ok) {
41+
const conversation: ApiGetConversationResponse = (
42+
await conversationResponse.json()
43+
).data;
44+
45+
return Ok(conversation);
46+
}
47+
if (conversationResponse.status === 404) {
48+
return Ok(null);
49+
}
50+
return Err(
51+
`Fetching conversation failed with error code ${conversationResponse.status}`,
52+
);
53+
} catch (error) {
54+
return Err(extractErrorMessageOrDefault(error));
55+
}
56+
};
57+
58+
/**
59+
* Create a conversation
60+
* @param accessToken
61+
* @param projectId
62+
* @param conversationName
63+
* @returns result containing the created conversation
64+
*/
65+
export const createConversation = async (
66+
accessToken: string,
67+
projectId: string,
68+
conversationName: string,
69+
): Promise<Result<ApiCreateConversationResponse, string>> => {
70+
try {
71+
const conversationResponse = await fetch(
72+
`${patternCoreEndpoint}/playground/conversation`,
73+
{
74+
method: 'POST',
75+
headers: {
76+
Authorization: `Bearer ${accessToken}`,
77+
'Content-Type': 'application/json',
78+
},
79+
body: JSON.stringify({
80+
name: conversationName,
81+
project_id: projectId,
82+
}),
83+
},
84+
);
85+
86+
if (conversationResponse.ok) {
87+
const conversation: ApiCreateConversationResponse = (
88+
await conversationResponse.json()
89+
).data;
90+
91+
return Ok(conversation);
92+
}
93+
return Err(
94+
`Creating conversation failed with error code ${conversationResponse.status}`,
95+
);
96+
} catch (error) {
97+
return Err(extractErrorMessageOrDefault(error));
98+
}
99+
};
100+
101+
/**
102+
* Send a message
103+
* @param accessToken
104+
* @param projectId
105+
* @param conversationId
106+
* @param message
107+
* @returns result containing model response
108+
*/
109+
export const sendMessage = async (
110+
accessToken: string,
111+
projectId: string,
112+
conversationId: string,
113+
message: string,
114+
): Promise<Result<ApiSendMessageResponse, string>> => {
115+
try {
116+
const messageResponse = await fetch(
117+
`${patternCoreEndpoint}/playground/conversation/${projectId}/${conversationId}/chat`,
118+
{
119+
method: 'POST',
120+
headers: {
121+
Authorization: `Bearer ${accessToken}`,
122+
'Content-Type': 'application/json',
123+
},
124+
body: JSON.stringify({
125+
message,
126+
message_type: 'text',
127+
stream: true,
128+
}),
129+
},
130+
);
131+
132+
if (messageResponse.ok) {
133+
const modelResponse: ApiSendMessageResponse = (
134+
await messageResponse.json()
135+
).data;
136+
137+
return Ok(modelResponse);
138+
}
139+
140+
return Err(
141+
`Sending message failed with error code ${messageResponse.status}`,
142+
);
143+
} catch (error) {
144+
return Err(extractErrorMessageOrDefault(error));
145+
}
146+
};
147+
148+
/**
149+
* Send a message and return a stream
150+
* @param accessToken
151+
* @param projectId
152+
* @param conversationId
153+
* @param message
154+
* @returns result containing the readable stream of response
155+
*/
156+
export const sendMessageStreamed = async (
157+
accessToken: string,
158+
projectId: string,
159+
conversationId: string,
160+
message: string,
161+
): Promise<Result<ApiSendMessageStreamedResponse, string>> => {
162+
try {
163+
const messageResponse = await fetch(
164+
`${patternCoreEndpoint}/playground/conversation/${projectId}/${conversationId}/chat`,
165+
{
166+
method: 'POST',
167+
headers: {
168+
Authorization: `Bearer ${accessToken}`,
169+
'Content-Type': 'application/json',
170+
},
171+
body: JSON.stringify({
172+
message,
173+
message_type: 'text',
174+
stream: true,
175+
}),
176+
},
177+
);
178+
179+
if (messageResponse.ok) {
180+
const responseStream = messageResponse.body;
181+
182+
return responseStream
183+
? Ok(messageResponse.body)
184+
: Err('Message was sent but stream object is null');
185+
}
186+
187+
return Err(
188+
`Sending message failed with error code ${messageResponse.status}`,
189+
);
190+
} catch (error) {
191+
return Err(extractErrorMessageOrDefault(error));
192+
}
193+
};

app/(chat)/api/chat/route.ts

Lines changed: 26 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,26 @@ import {
66
} from 'ai';
77

88
import { auth } from '@/app/(auth)/auth';
9-
import { myProvider } from '@/lib/ai/models';
10-
import { systemPrompt } from '@/lib/ai/prompts';
11-
import {
12-
deleteChatById,
13-
getChatById,
14-
saveChat,
15-
saveMessages,
16-
} from '@/lib/db/queries';
17-
import {
18-
generateUUID,
19-
getMostRecentUserMessage,
20-
sanitizeResponseMessages,
21-
} from '@/lib/utils';
9+
import { patternProvider } from '@/lib/ai/pattern-provider';
10+
import { deleteChatById, getChatById } from '@/lib/db/queries';
11+
import { generateUUID, getMostRecentUserMessage } from '@/lib/utils';
2212

23-
import { generateTitleFromUserMessage } from '../../actions';
24-
import { createDocument } from '@/lib/ai/tools/create-document';
25-
import { updateDocument } from '@/lib/ai/tools/update-document';
26-
import { requestSuggestions } from '@/lib/ai/tools/request-suggestions';
27-
import { getWeather } from '@/lib/ai/tools/get-weather';
13+
import { getOrCreateConversation } from '../../service';
2814

2915
export const maxDuration = 60;
3016

3117
export async function POST(request: Request) {
32-
const {
33-
id,
34-
messages,
35-
selectedChatModel,
36-
}: { id: string; messages: Array<Message>; selectedChatModel: string } =
18+
const { id, messages }: { id: string; messages: Array<Message> } =
3719
await request.json();
3820

3921
const session = await auth();
4022

41-
if (!session || !session.user || !session.user.id) {
23+
if (
24+
!session ||
25+
!session.chainId ||
26+
!session.address ||
27+
!session.accessToken
28+
) {
4229
return new Response('Unauthorized', { status: 401 });
4330
}
4431

@@ -48,71 +35,31 @@ export async function POST(request: Request) {
4835
return new Response('No user message found', { status: 400 });
4936
}
5037

51-
const chat = await getChatById({ id });
38+
const conversationResult = await getOrCreateConversation(
39+
session.accessToken,
40+
session.projectId,
41+
id,
42+
);
5243

53-
if (!chat) {
54-
const title = await generateTitleFromUserMessage({ message: userMessage });
55-
await saveChat({ id, userId: session.user.id, title });
44+
if (conversationResult.isErr()) {
45+
return new Response(conversationResult.error, { status: 400 });
5646
}
5747

58-
await saveMessages({
59-
messages: [{ ...userMessage, createdAt: new Date(), chatId: id }],
60-
});
48+
const conversation = conversationResult.value;
6149

6250
return createDataStreamResponse({
6351
execute: (dataStream) => {
6452
const result = streamText({
65-
model: myProvider.languageModel(selectedChatModel),
66-
system: systemPrompt({ selectedChatModel }),
53+
model: patternProvider(),
6754
messages,
68-
maxSteps: 5,
69-
experimental_activeTools:
70-
selectedChatModel === 'chat-model-reasoning'
71-
? []
72-
: [
73-
'getWeather',
74-
'createDocument',
75-
'updateDocument',
76-
'requestSuggestions',
77-
],
7855
experimental_transform: smoothStream({ chunking: 'word' }),
7956
experimental_generateMessageId: generateUUID,
80-
tools: {
81-
getWeather,
82-
createDocument: createDocument({ session, dataStream }),
83-
updateDocument: updateDocument({ session, dataStream }),
84-
requestSuggestions: requestSuggestions({
85-
session,
86-
dataStream,
87-
}),
88-
},
89-
onFinish: async ({ response, reasoning }) => {
90-
if (session.user?.id) {
91-
try {
92-
const sanitizedResponseMessages = sanitizeResponseMessages({
93-
messages: response.messages,
94-
reasoning,
95-
});
96-
97-
await saveMessages({
98-
messages: sanitizedResponseMessages.map((message) => {
99-
return {
100-
id: message.id,
101-
chatId: id,
102-
role: message.role,
103-
content: message.content,
104-
createdAt: new Date(),
105-
};
106-
}),
107-
});
108-
} catch (error) {
109-
console.error('Failed to save chat');
110-
}
111-
}
112-
},
113-
experimental_telemetry: {
114-
isEnabled: true,
115-
functionId: 'stream-text',
57+
providerOptions: {
58+
pattern: {
59+
conversationId: conversation.id,
60+
accessToken: session.accessToken,
61+
projectId: session.projectId,
62+
},
11663
},
11764
});
11865

app/(chat)/service.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { type Result, Err, Ok } from 'ts-results-es';
2+
3+
import { createConversation, getConversation } from './adapter';
4+
import type { Conversation } from './types';
5+
6+
/**
7+
* Checks if the conversation exists and returns it, otherwise creates it
8+
* @param accessToken
9+
* @returns result containing the existing or created conversation
10+
*/
11+
export const getOrCreateConversation = async (
12+
accessToken: string,
13+
projectId: string,
14+
conversationId: string,
15+
): Promise<Result<Conversation, string>> => {
16+
const conversationResult = await getConversation(
17+
accessToken,
18+
projectId,
19+
conversationId,
20+
);
21+
if (conversationResult.isErr()) {
22+
return Err(conversationResult.error);
23+
}
24+
25+
let conversation = conversationResult.value;
26+
27+
if (!conversation) {
28+
const createConversationResult = await createConversation(
29+
accessToken,
30+
projectId,
31+
'Default Title',
32+
);
33+
if (createConversationResult.isErr()) {
34+
return Err(createConversationResult.error);
35+
}
36+
37+
conversation = createConversationResult.value;
38+
}
39+
40+
return Ok(conversation);
41+
};
42+
43+
export { sendMessage, sendMessageStreamed } from './adapter';

app/(chat)/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export interface Conversation {
2+
id: string;
3+
name: string;
4+
project_id: string;
5+
}
6+
7+
export type ApiGetConversationResponse = Conversation | null;
8+
export type ApiCreateConversationResponse = Conversation;
9+
export type ApiSendMessageResponse = string;
10+
export type ApiSendMessageStreamedResponse = ReadableStream;

0 commit comments

Comments
 (0)