@coursebuilder/server 0.1.0-canary.0.0c9beb2ae
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/LICENSE +21 -0
- package/dist/actions/checkout.d.ts +6 -0
- package/dist/actions/checkout.js +9 -0
- package/dist/actions/checkout.js.map +1 -0
- package/dist/actions/claimed.d.ts +6 -0
- package/dist/actions/claimed.js +8 -0
- package/dist/actions/claimed.js.map +1 -0
- package/dist/actions/create-magic-link.d.ts +6 -0
- package/dist/actions/create-magic-link.js +9 -0
- package/dist/actions/create-magic-link.js.map +1 -0
- package/dist/actions/index.d.ts +11 -0
- package/dist/actions/index.js +18 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/prices-coupon-for-code.d.ts +6 -0
- package/dist/actions/prices-coupon-for-code.js +63 -0
- package/dist/actions/prices-coupon-for-code.js.map +1 -0
- package/dist/actions/prices-formatted.d.ts +6 -0
- package/dist/actions/prices-formatted.js +8 -0
- package/dist/actions/prices-formatted.js.map +1 -0
- package/dist/actions/process-refund.d.ts +1 -0
- package/dist/actions/process-refund.js +8 -0
- package/dist/actions/process-refund.js.map +1 -0
- package/dist/actions/redeem.d.ts +1 -0
- package/dist/actions/redeem.js +8 -0
- package/dist/actions/redeem.js.map +1 -0
- package/dist/actions/transfer-purchase.d.ts +1 -0
- package/dist/actions/transfer-purchase.js +8 -0
- package/dist/actions/transfer-purchase.js.map +1 -0
- package/dist/actions/user-lookup.d.ts +6 -0
- package/dist/actions/user-lookup.js +9 -0
- package/dist/actions/user-lookup.js.map +1 -0
- package/dist/adapter-BtDS6LDw.d.ts +9 -0
- package/dist/chunk-22AMOZYZ.js +67 -0
- package/dist/chunk-22AMOZYZ.js.map +1 -0
- package/dist/chunk-4EQQSW57.js +32 -0
- package/dist/chunk-4EQQSW57.js.map +1 -0
- package/dist/chunk-4LZZLN6T.js +52 -0
- package/dist/chunk-4LZZLN6T.js.map +1 -0
- package/dist/chunk-4QQA3TC2.js +29 -0
- package/dist/chunk-4QQA3TC2.js.map +1 -0
- package/dist/chunk-4TSEPUSC.js +160 -0
- package/dist/chunk-4TSEPUSC.js.map +1 -0
- package/dist/chunk-5O4UO4IR.js +93 -0
- package/dist/chunk-5O4UO4IR.js.map +1 -0
- package/dist/chunk-7FCQOLKL.js +26 -0
- package/dist/chunk-7FCQOLKL.js.map +1 -0
- package/dist/chunk-AZLUMIIF.js +8 -0
- package/dist/chunk-AZLUMIIF.js.map +1 -0
- package/dist/chunk-E6HV6TNI.js +38 -0
- package/dist/chunk-E6HV6TNI.js.map +1 -0
- package/dist/chunk-ECNJPNPU.js +54 -0
- package/dist/chunk-ECNJPNPU.js.map +1 -0
- package/dist/chunk-F3KUYN5I.js +111 -0
- package/dist/chunk-F3KUYN5I.js.map +1 -0
- package/dist/chunk-G63YAJF2.js +107 -0
- package/dist/chunk-G63YAJF2.js.map +1 -0
- package/dist/chunk-JFR7XO72.js +7 -0
- package/dist/chunk-JFR7XO72.js.map +1 -0
- package/dist/chunk-KNC7QVGZ.js +26 -0
- package/dist/chunk-KNC7QVGZ.js.map +1 -0
- package/dist/chunk-ODSSRMD2.js +270 -0
- package/dist/chunk-ODSSRMD2.js.map +1 -0
- package/dist/chunk-VEKYIR7I.js +26 -0
- package/dist/chunk-VEKYIR7I.js.map +1 -0
- package/dist/chunk-W7YAT4NR.js +12 -0
- package/dist/chunk-W7YAT4NR.js.map +1 -0
- package/dist/chunk-WGAPYIUP.js +56 -0
- package/dist/chunk-WGAPYIUP.js.map +1 -0
- package/dist/chunk-WYNH2AWD.js +7 -0
- package/dist/chunk-WYNH2AWD.js.map +1 -0
- package/dist/chunk-Y5X5J55Z.js +384 -0
- package/dist/chunk-Y5X5J55Z.js.map +1 -0
- package/dist/chunk-YFLGJIMA.js +216 -0
- package/dist/chunk-YFLGJIMA.js.map +1 -0
- package/dist/chunk-ZFZNN4E5.js +7 -0
- package/dist/chunk-ZFZNN4E5.js.map +1 -0
- package/dist/create-inngest-middleware.d.ts +635 -0
- package/dist/create-inngest-middleware.js +12 -0
- package/dist/create-inngest-middleware.js.map +1 -0
- package/dist/http-router.d.ts +20 -0
- package/dist/http-router.js +24 -0
- package/dist/http-router.js.map +1 -0
- package/dist/http.d.ts +38 -0
- package/dist/http.js +32 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +365 -0
- package/dist/index.js.map +1 -0
- package/dist/magic-string.es-ENO43NON.js +1389 -0
- package/dist/magic-string.es-ENO43NON.js.map +1 -0
- package/dist/runtime/actions.d.ts +5 -0
- package/dist/runtime/actions.js +8 -0
- package/dist/runtime/actions.js.map +1 -0
- package/dist/runtime/assert.d.ts +15 -0
- package/dist/runtime/assert.js +8 -0
- package/dist/runtime/assert.js.map +1 -0
- package/dist/runtime/cookie.d.ts +8 -0
- package/dist/runtime/cookie.js +8 -0
- package/dist/runtime/cookie.js.map +1 -0
- package/dist/runtime/env.d.ts +9 -0
- package/dist/runtime/env.js +11 -0
- package/dist/runtime/env.js.map +1 -0
- package/dist/runtime/env.test.d.ts +2 -0
- package/dist/runtime/env.test.js +14956 -0
- package/dist/runtime/env.test.js.map +1 -0
- package/dist/runtime/init.d.ts +26 -0
- package/dist/runtime/init.js +14 -0
- package/dist/runtime/init.js.map +1 -0
- package/dist/runtime/logger.d.ts +4 -0
- package/dist/runtime/logger.js +10 -0
- package/dist/runtime/logger.js.map +1 -0
- package/dist/runtime/merge.d.ts +4 -0
- package/dist/runtime/merge.js +8 -0
- package/dist/runtime/merge.js.map +1 -0
- package/dist/runtime/providers.d.ts +21 -0
- package/dist/runtime/providers.js +9 -0
- package/dist/runtime/providers.js.map +1 -0
- package/dist/runtime/web.d.ts +29 -0
- package/dist/runtime/web.js +16 -0
- package/dist/runtime/web.js.map +1 -0
- package/package.json +65 -0
- package/src/actions/actions.test.ts +232 -0
- package/src/actions/checkout.ts +160 -0
- package/src/actions/claimed.ts +40 -0
- package/src/actions/create-magic-link.ts +78 -0
- package/src/actions/index.ts +401 -0
- package/src/actions/prices-coupon-for-code.ts +91 -0
- package/src/actions/prices-formatted.ts +313 -0
- package/src/actions/process-refund.ts +2 -0
- package/src/actions/redeem.ts +2 -0
- package/src/actions/transfer-purchase.ts +2 -0
- package/src/actions/user-lookup.ts +37 -0
- package/src/adapter.ts +28 -0
- package/src/create-inngest-middleware.ts +157 -0
- package/src/http-actions.ts +29 -0
- package/src/http-router.ts +79 -0
- package/src/http.test.ts +46 -0
- package/src/http.ts +83 -0
- package/src/index.test.ts +21 -0
- package/src/index.ts +44 -0
- package/src/mock-adapter.ts +398 -0
- package/src/mock-payment-provider.ts +56 -0
- package/src/resource-chat.ts +371 -0
- package/src/runtime/actions.ts +25 -0
- package/src/runtime/assert.ts +55 -0
- package/src/runtime/cookie.ts +12 -0
- package/src/runtime/env.test.ts +49 -0
- package/src/runtime/env.ts +63 -0
- package/src/runtime/init.ts +112 -0
- package/src/runtime/logger.ts +6 -0
- package/src/runtime/merge.ts +25 -0
- package/src/runtime/providers.ts +38 -0
- package/src/runtime/web.ts +199 -0
- package/src/streaming-chat-prompt-executor.ts +43 -0
- package/src/type-contracts.test.ts +167 -0
- package/src/web-body-parse.test.ts +55 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { NonRetriableError } from 'inngest'
|
|
2
|
+
import { Liquid } from 'liquidjs'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
import type { ServerAdapter } from './adapter'
|
|
6
|
+
import { logger } from './runtime/logger'
|
|
7
|
+
import { LlmProviderConfig } from '@coursebuilder/core/providers/openai'
|
|
8
|
+
import { PartyProviderConfig } from '@coursebuilder/core/providers/partykit'
|
|
9
|
+
import { User } from '@coursebuilder/core/schemas'
|
|
10
|
+
import {
|
|
11
|
+
CoreInngestFunctionInput,
|
|
12
|
+
CoreInngestHandler,
|
|
13
|
+
CoreInngestTrigger,
|
|
14
|
+
} from './create-inngest-middleware'
|
|
15
|
+
import {
|
|
16
|
+
ChatCompletionRequestMessage,
|
|
17
|
+
streamingChatPromptExecutor,
|
|
18
|
+
} from './streaming-chat-prompt-executor'
|
|
19
|
+
|
|
20
|
+
/** @deprecated Import event contracts from `@coursebuilder/core/events`. */
|
|
21
|
+
export {
|
|
22
|
+
ChatResourceSchema,
|
|
23
|
+
type ChatResource,
|
|
24
|
+
RESOURCE_CHAT_REQUEST_EVENT,
|
|
25
|
+
type ResourceChat,
|
|
26
|
+
} from '@coursebuilder/core/events/resource-chat'
|
|
27
|
+
import {
|
|
28
|
+
ChatResourceSchema,
|
|
29
|
+
type ChatResource,
|
|
30
|
+
RESOURCE_CHAT_REQUEST_EVENT,
|
|
31
|
+
} from '@coursebuilder/core/events/resource-chat'
|
|
32
|
+
|
|
33
|
+
export const resourceChatConfig = {
|
|
34
|
+
id: `resource-chat`,
|
|
35
|
+
name: 'Resource Chat',
|
|
36
|
+
rateLimit: {
|
|
37
|
+
key: 'event.user.id',
|
|
38
|
+
limit: 5,
|
|
39
|
+
period: '1m' as `${number}m`,
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const resourceChatTrigger: CoreInngestTrigger = {
|
|
44
|
+
event: RESOURCE_CHAT_REQUEST_EVENT,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Define ChatCompletionRequestMessageRoleEnum
|
|
48
|
+
export enum ChatCompletionRequestMessageRoleEnum {
|
|
49
|
+
System = 'system',
|
|
50
|
+
User = 'user',
|
|
51
|
+
Assistant = 'assistant',
|
|
52
|
+
Tool = 'tool',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* TODO: Cancellation conditions need to be added $$
|
|
57
|
+
*/
|
|
58
|
+
export const resourceChatHandler: CoreInngestHandler = async ({
|
|
59
|
+
event,
|
|
60
|
+
step,
|
|
61
|
+
openaiProvider,
|
|
62
|
+
partyProvider,
|
|
63
|
+
db,
|
|
64
|
+
}: CoreInngestFunctionInput) => {
|
|
65
|
+
const resourceId = event.data.resourceId
|
|
66
|
+
const workflowTrigger = event.data.selectedWorkflow
|
|
67
|
+
|
|
68
|
+
const currentUserMessage = event.data.messages[event.data.messages.length - 1]
|
|
69
|
+
|
|
70
|
+
if (currentUserMessage?.content) {
|
|
71
|
+
await step.run(
|
|
72
|
+
`partykit broadcast user prompt [${resourceId}]`,
|
|
73
|
+
async () => {
|
|
74
|
+
await partyProvider.broadcastMessage({
|
|
75
|
+
body: {
|
|
76
|
+
body: currentUserMessage.content,
|
|
77
|
+
requestId: resourceId,
|
|
78
|
+
name: 'resource.chat.prompted',
|
|
79
|
+
// @ts-expect-error
|
|
80
|
+
userId: event.user.id,
|
|
81
|
+
},
|
|
82
|
+
roomId: resourceId,
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const resource = await step.run('get the resource', async () => {
|
|
89
|
+
return db.getContentResource(resourceId)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
if (!resource) {
|
|
93
|
+
throw new NonRetriableError(`Resource not found for id (${resourceId})`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const videoResource = await step.run('get the video resource', async () => {
|
|
97
|
+
return db.getVideoResource(resource.resources?.[0]?.resource.id)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const messages = await resourceChatWorkflowExecutor({
|
|
101
|
+
db,
|
|
102
|
+
openaiProvider,
|
|
103
|
+
partyProvider,
|
|
104
|
+
step,
|
|
105
|
+
workflowTrigger,
|
|
106
|
+
resourceId,
|
|
107
|
+
|
|
108
|
+
resource: {
|
|
109
|
+
...videoResource,
|
|
110
|
+
...resource,
|
|
111
|
+
...resource.fields,
|
|
112
|
+
resources: resource.resources ?? [],
|
|
113
|
+
},
|
|
114
|
+
messages: event.data.messages,
|
|
115
|
+
// @ts-expect-error
|
|
116
|
+
user: event.user,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
return { resource: { ...videoResource, ...resource }, messages }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* loads the workflow from sanity based on the trigger and then executes the workflow using the `resource` as
|
|
124
|
+
* input that is parsed by liquid into the prompt
|
|
125
|
+
*
|
|
126
|
+
* This treats the system prompt as "special" from the loaded workflow since it is added to the beginning of the prompt
|
|
127
|
+
* in the chat context, but additional steps are executed as normal
|
|
128
|
+
*
|
|
129
|
+
* @param step
|
|
130
|
+
* @param workflowTrigger
|
|
131
|
+
* @param resourceId
|
|
132
|
+
* @param messages
|
|
133
|
+
* @param resource
|
|
134
|
+
* @param currentFeedback
|
|
135
|
+
*/
|
|
136
|
+
export async function resourceChatWorkflowExecutor({
|
|
137
|
+
step,
|
|
138
|
+
workflowTrigger,
|
|
139
|
+
resourceId,
|
|
140
|
+
messages,
|
|
141
|
+
resource,
|
|
142
|
+
user,
|
|
143
|
+
openaiProvider,
|
|
144
|
+
partyProvider,
|
|
145
|
+
db,
|
|
146
|
+
model,
|
|
147
|
+
}: {
|
|
148
|
+
openaiProvider: LlmProviderConfig
|
|
149
|
+
partyProvider: PartyProviderConfig
|
|
150
|
+
db: ServerAdapter
|
|
151
|
+
resource: ChatResource
|
|
152
|
+
step: any
|
|
153
|
+
workflowTrigger: string
|
|
154
|
+
resourceId: string
|
|
155
|
+
messages: ChatCompletionRequestMessage[]
|
|
156
|
+
user: User
|
|
157
|
+
model?: string
|
|
158
|
+
}) {
|
|
159
|
+
const basicPrompt = {
|
|
160
|
+
id: 'basic',
|
|
161
|
+
name: 'Basic',
|
|
162
|
+
type: 'prompt',
|
|
163
|
+
fields: {
|
|
164
|
+
body: `# Instructions
|
|
165
|
+
Pause. Take a deep breath and think. This is important. Think step by step.
|
|
166
|
+
|
|
167
|
+
You are serving as a writing assistant for a content creator that is publishing a {% if transcript %}video based{% endif %} post for software developers. The content creator will ask questions and expect concise answers that aren't corny or generic.
|
|
168
|
+
|
|
169
|
+
Keep responses scoped to the post and it's direct contents. Do not include additional information.
|
|
170
|
+
Do not include information that is not directly related to the post or the {% if transcript %}video{% endif %}.
|
|
171
|
+
|
|
172
|
+
Use simple language.
|
|
173
|
+
|
|
174
|
+
Use only most popular 2000 english words {% if transcript %}and words used in the transcript{% endif %}.
|
|
175
|
+
Simple is better.
|
|
176
|
+
Flourish is bad.
|
|
177
|
+
**People will hate you if you try to sound clever.**
|
|
178
|
+
|
|
179
|
+
{% if wordLevelSrt %} add screenshots from the video when they are relevant and would be useful to
|
|
180
|
+
the reader using the following template replacing {{timestampInSeconds}} with the time noted in the transcript:
|
|
181
|
+
|
|
182
|
+

|
|
183
|
+
be precise with the timestamps! {% if wordLevelSrt %} use this word level SRT to get the exact timestamp:
|
|
184
|
+
|
|
185
|
+
word level srt start
|
|
186
|
+
{{wordLevelSrt}} {% endif %}
|
|
187
|
+
|
|
188
|
+
word level srt end
|
|
189
|
+
{% endif %}
|
|
190
|
+
|
|
191
|
+
Get it right and there's $200 cash gratuity in it for you ;)
|
|
192
|
+
|
|
193
|
+
{% if transcript %}The goal is to build a really good written version of the existing video, not edit the video itself. The video is done.
|
|
194
|
+
|
|
195
|
+
The post transcript is the final representation of the video. The post transcript is the source of truth.{% endif %}
|
|
196
|
+
|
|
197
|
+
Keep the language simple and don't use words not in the post {% if transcript %}transcript{% endif %}.
|
|
198
|
+
|
|
199
|
+
{% if transcript %}Do not make direct references to the video.
|
|
200
|
+
Do not make direct references to the transcript.{% endif %}
|
|
201
|
+
Do not make direct references to the content creator.
|
|
202
|
+
Do not make direct references to 'the post'.
|
|
203
|
+
Jump right to the point.
|
|
204
|
+
|
|
205
|
+
Post Type: {{type}}
|
|
206
|
+
|
|
207
|
+
Post Title: {{title}}
|
|
208
|
+
|
|
209
|
+
{% if transcript %}Post Transcript: {{transcript}}{% endif %}
|
|
210
|
+
|
|
211
|
+
Post Body: {{body}}
|
|
212
|
+
`,
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let prompt =
|
|
217
|
+
workflowTrigger === 'basic'
|
|
218
|
+
? basicPrompt
|
|
219
|
+
: await step.run('Load Prompt', async () => {
|
|
220
|
+
return db.getContentResource(workflowTrigger)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
if (!prompt) {
|
|
224
|
+
prompt = basicPrompt
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let systemPrompt: ChatCompletionRequestMessage = {
|
|
228
|
+
role: 'system',
|
|
229
|
+
content: prompt.fields.body,
|
|
230
|
+
}
|
|
231
|
+
let seedMessages: ChatCompletionRequestMessage[] = []
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const actionParsed = z
|
|
235
|
+
.array(
|
|
236
|
+
z.object({
|
|
237
|
+
role: z.enum([
|
|
238
|
+
ChatCompletionRequestMessageRoleEnum.System,
|
|
239
|
+
ChatCompletionRequestMessageRoleEnum.User,
|
|
240
|
+
ChatCompletionRequestMessageRoleEnum.Assistant,
|
|
241
|
+
ChatCompletionRequestMessageRoleEnum.Tool,
|
|
242
|
+
]),
|
|
243
|
+
content: z.string(),
|
|
244
|
+
}),
|
|
245
|
+
)
|
|
246
|
+
.parse(JSON.parse(prompt.fields.body)) as Array<{
|
|
247
|
+
role: ChatCompletionRequestMessageRoleEnum
|
|
248
|
+
content: string
|
|
249
|
+
}>
|
|
250
|
+
|
|
251
|
+
const actionMessages: ChatCompletionRequestMessage[] = []
|
|
252
|
+
for (const actionMessage of actionParsed) {
|
|
253
|
+
const liquidParsedContent = await step.run(
|
|
254
|
+
'parse json content',
|
|
255
|
+
async () => {
|
|
256
|
+
const engine = new Liquid()
|
|
257
|
+
const contentString =
|
|
258
|
+
typeof actionMessage.content === 'string'
|
|
259
|
+
? actionMessage.content
|
|
260
|
+
: JSON.stringify(actionMessage.content)
|
|
261
|
+
return await engine.parseAndRender(contentString, {
|
|
262
|
+
...resource,
|
|
263
|
+
})
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
actionMessages.push({
|
|
268
|
+
role: actionMessage.role,
|
|
269
|
+
content: liquidParsedContent,
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
if (actionMessages.length > 0) {
|
|
273
|
+
;[
|
|
274
|
+
systemPrompt = {
|
|
275
|
+
role: 'system',
|
|
276
|
+
content: prompt.fields.body,
|
|
277
|
+
},
|
|
278
|
+
...seedMessages
|
|
279
|
+
] = actionMessages
|
|
280
|
+
}
|
|
281
|
+
} catch (e: any) {
|
|
282
|
+
// if the prompt action content is not valid json, we assume it's just text
|
|
283
|
+
systemPrompt = await step.run(`parse system prompt`, async () => {
|
|
284
|
+
try {
|
|
285
|
+
const engine = new Liquid()
|
|
286
|
+
const originalSystemPrompt = systemPrompt // Store reference before reassignment
|
|
287
|
+
const contentString =
|
|
288
|
+
typeof originalSystemPrompt.content === 'string'
|
|
289
|
+
? originalSystemPrompt.content
|
|
290
|
+
: JSON.stringify(originalSystemPrompt.content)
|
|
291
|
+
return {
|
|
292
|
+
role: originalSystemPrompt.role,
|
|
293
|
+
content: await engine.parseAndRender(contentString || '', resource),
|
|
294
|
+
}
|
|
295
|
+
} catch (e: any) {
|
|
296
|
+
logger.error(e instanceof Error ? e : new Error(String(e)), {
|
|
297
|
+
context: 'co-gardener.resource-chat.system-prompt-render',
|
|
298
|
+
resourceId,
|
|
299
|
+
})
|
|
300
|
+
return {
|
|
301
|
+
role: 'system',
|
|
302
|
+
content: prompt.fields.body,
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (messages.length <= 2 && systemPrompt) {
|
|
309
|
+
messages = [systemPrompt, ...seedMessages, ...messages]
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const currentUserMessage = messages[messages.length - 1]
|
|
313
|
+
const currentResourceMetadata = messages[messages.length - 2]
|
|
314
|
+
|
|
315
|
+
messages = await step.run('answer the user prompt', async () => {
|
|
316
|
+
if (!currentUserMessage) {
|
|
317
|
+
throw new Error('No user message')
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const engine = new Liquid()
|
|
321
|
+
const currentUserContentString =
|
|
322
|
+
typeof currentUserMessage.content === 'string'
|
|
323
|
+
? currentUserMessage.content
|
|
324
|
+
: JSON.stringify(currentUserMessage.content)
|
|
325
|
+
|
|
326
|
+
currentUserMessage.content = await engine.parseAndRender(
|
|
327
|
+
currentUserContentString ?? '',
|
|
328
|
+
resource,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
if (currentResourceMetadata) {
|
|
332
|
+
const metadataContentString =
|
|
333
|
+
typeof currentResourceMetadata.content === 'string'
|
|
334
|
+
? currentResourceMetadata.content
|
|
335
|
+
: JSON.stringify(currentResourceMetadata.content)
|
|
336
|
+
currentResourceMetadata.content = await engine.parseAndRender(
|
|
337
|
+
metadataContentString ?? '',
|
|
338
|
+
resource,
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return streamingChatPromptExecutor({
|
|
343
|
+
requestId: resourceId,
|
|
344
|
+
promptMessages: messages,
|
|
345
|
+
model: prompt.fields.model || model || 'gpt-4-turbo',
|
|
346
|
+
provider: openaiProvider,
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
await step.run(`partykit broadcast [${resourceId}]`, async () => {
|
|
351
|
+
return await partyProvider.broadcastMessage({
|
|
352
|
+
body: {
|
|
353
|
+
body: messages,
|
|
354
|
+
requestId: resourceId,
|
|
355
|
+
name: 'resource.chat.completed',
|
|
356
|
+
metadata: {
|
|
357
|
+
workflow: workflowTrigger,
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
roomId: resourceId,
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
return messages
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export const resourceChat = {
|
|
368
|
+
config: resourceChatConfig,
|
|
369
|
+
trigger: resourceChatTrigger,
|
|
370
|
+
handler: resourceChatHandler,
|
|
371
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CourseBuilderAction } from '@coursebuilder/core/types'
|
|
2
|
+
|
|
3
|
+
const actions: CourseBuilderAction[] = [
|
|
4
|
+
'webhook',
|
|
5
|
+
'srt',
|
|
6
|
+
'session',
|
|
7
|
+
'subscribe-to-list',
|
|
8
|
+
'checkout',
|
|
9
|
+
'redeem',
|
|
10
|
+
'prices-formatted',
|
|
11
|
+
'subscriber',
|
|
12
|
+
'purchases',
|
|
13
|
+
'lookup',
|
|
14
|
+
'claimed',
|
|
15
|
+
'nameUpdate',
|
|
16
|
+
'transfer',
|
|
17
|
+
'refund',
|
|
18
|
+
'create-magic-link',
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
export function isCourseBuilderAction(
|
|
22
|
+
action: string,
|
|
23
|
+
): action is CourseBuilderAction {
|
|
24
|
+
return actions.includes(action as CourseBuilderAction)
|
|
25
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ServerAdapter } from '../adapter'
|
|
2
|
+
import {
|
|
3
|
+
MissingAdapter,
|
|
4
|
+
MissingAdapterMethods,
|
|
5
|
+
UnsupportedStrategy,
|
|
6
|
+
} from '@coursebuilder/core/errors'
|
|
7
|
+
import { RequestInternal } from '@coursebuilder/core/types'
|
|
8
|
+
|
|
9
|
+
import { CourseBuilderConfig } from '../http'
|
|
10
|
+
import { WarningCode } from './logger'
|
|
11
|
+
|
|
12
|
+
type ConfigError = MissingAdapter | MissingAdapterMethods | UnsupportedStrategy
|
|
13
|
+
|
|
14
|
+
let hasTranscript = false
|
|
15
|
+
let warned = false
|
|
16
|
+
|
|
17
|
+
export function assertConfig(
|
|
18
|
+
request: RequestInternal,
|
|
19
|
+
options: CourseBuilderConfig,
|
|
20
|
+
): ConfigError | WarningCode[] {
|
|
21
|
+
const { url } = request
|
|
22
|
+
const warnings: WarningCode[] = []
|
|
23
|
+
|
|
24
|
+
if (!warned && options.debug) warnings.push('debug-enabled')
|
|
25
|
+
|
|
26
|
+
// Keep track of webauthn providers that use conditional UI
|
|
27
|
+
|
|
28
|
+
for (const p of options.providers) {
|
|
29
|
+
const provider = typeof p === 'function' ? p() : p
|
|
30
|
+
if (provider.type === 'transcription') {
|
|
31
|
+
hasTranscript = true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { adapter } = options
|
|
36
|
+
|
|
37
|
+
let requiredMethods: (keyof ServerAdapter)[] = []
|
|
38
|
+
|
|
39
|
+
if (hasTranscript) {
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (adapter) {
|
|
43
|
+
const missing = requiredMethods.filter((m) => !(m in adapter))
|
|
44
|
+
|
|
45
|
+
if (missing.length) {
|
|
46
|
+
return new MissingAdapterMethods(
|
|
47
|
+
`Required adapter methods were missing: ${missing.join(', ')}`,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!warned) warned = true
|
|
53
|
+
|
|
54
|
+
return warnings
|
|
55
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CookieOption, CookiesOptions } from '@coursebuilder/core/types'
|
|
2
|
+
|
|
3
|
+
export interface Cookie extends CookieOption {
|
|
4
|
+
value: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function defaultCookies(useSecureCookies: boolean) {
|
|
8
|
+
const cookiePrefix = useSecureCookies ? '__Secure-' : ''
|
|
9
|
+
return {
|
|
10
|
+
// default cookie options
|
|
11
|
+
} as const satisfies CookiesOptions
|
|
12
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { CourseBuilderConfig } from '../http'
|
|
4
|
+
import { setEnvDefaults } from './env'
|
|
5
|
+
import Deepgram from '@coursebuilder/core/providers/deepgram'
|
|
6
|
+
|
|
7
|
+
const testConfig: CourseBuilderConfig = {
|
|
8
|
+
providers: [Deepgram],
|
|
9
|
+
baseUrl: '/coursebuilder',
|
|
10
|
+
authConfig: {},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let courseBuilderConfig: CourseBuilderConfig
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
courseBuilderConfig = { ...testConfig } // clone
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('config is inferred from environment variables', () => {
|
|
20
|
+
it('providers (client id, client secret, issuer, api key)', () => {
|
|
21
|
+
const env = {
|
|
22
|
+
COURSEBUILDER_DEEPGRAM_API_KEY: 'fdsa',
|
|
23
|
+
}
|
|
24
|
+
setEnvDefaults(env, courseBuilderConfig)
|
|
25
|
+
const [p1] = courseBuilderConfig.providers
|
|
26
|
+
// @ts-expect-error
|
|
27
|
+
expect(p1.apiKey).toBe(env.COURSEBUILDER_DEEPGRAM_API_KEY)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('COURSEBUILDER_URL', () => {
|
|
31
|
+
const env = { COURSEBUILDER_URL: 'http://n/api/coursebuilder' }
|
|
32
|
+
setEnvDefaults(env, courseBuilderConfig)
|
|
33
|
+
expect(courseBuilderConfig.basePath).toBe('/api/coursebuilder')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('COURSEBUILDER_URL + prefer config', () => {
|
|
37
|
+
const env = { COURSEBUILDER_URL: 'http://n/api/coursebuilder' }
|
|
38
|
+
const fromConfig = '/basepath-from-config'
|
|
39
|
+
courseBuilderConfig.basePath = fromConfig
|
|
40
|
+
setEnvDefaults(env, courseBuilderConfig)
|
|
41
|
+
expect(courseBuilderConfig.basePath).toBe(fromConfig)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('COURSEBUILDER_URL, but invalid value', () => {
|
|
45
|
+
const env = { COURSEBUILDER_URL: 'secret' }
|
|
46
|
+
setEnvDefaults(env, courseBuilderConfig)
|
|
47
|
+
expect(courseBuilderConfig.basePath).toBe('/coursebuilder')
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { CourseBuilderAction } from '@coursebuilder/core/types'
|
|
2
|
+
|
|
3
|
+
import { CourseBuilderConfig } from '../http'
|
|
4
|
+
import { logger } from './logger.js'
|
|
5
|
+
|
|
6
|
+
/** Set default env variables on the config object */
|
|
7
|
+
export function setEnvDefaults(envObject: any, config: CourseBuilderConfig) {
|
|
8
|
+
try {
|
|
9
|
+
const url = envObject.COURSEBUILDER_URL
|
|
10
|
+
if (url && !config.basePath) config.basePath = new URL(url).pathname
|
|
11
|
+
} catch {
|
|
12
|
+
} finally {
|
|
13
|
+
config.basePath ??= `/coursebuilder`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
config.providers = config.providers.map((p) => {
|
|
17
|
+
const finalProvider = typeof p === 'function' ? p({}) : p
|
|
18
|
+
const ID = finalProvider.id.toUpperCase()
|
|
19
|
+
if (finalProvider.type === 'transcription') {
|
|
20
|
+
finalProvider.apiKey ??= envObject[`COURSEBUILDER_${ID}_API_KEY`]
|
|
21
|
+
}
|
|
22
|
+
return finalProvider
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createActionURL(
|
|
27
|
+
action: CourseBuilderAction,
|
|
28
|
+
protocol: string,
|
|
29
|
+
headers: Headers,
|
|
30
|
+
envObject: any,
|
|
31
|
+
basePath?: string,
|
|
32
|
+
): URL {
|
|
33
|
+
let envUrl = envObject.COURSEBUILDER_URL
|
|
34
|
+
|
|
35
|
+
let url: URL
|
|
36
|
+
if (envUrl) {
|
|
37
|
+
url = new URL(envUrl)
|
|
38
|
+
if (basePath && basePath !== '/' && url.pathname !== '/') {
|
|
39
|
+
logger.debug(
|
|
40
|
+
url.pathname === basePath
|
|
41
|
+
? 'env-url-basepath-redundant'
|
|
42
|
+
: 'env-url-basepath-mismatch',
|
|
43
|
+
)
|
|
44
|
+
url.pathname = '/'
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
const detectedHost = headers.get('x-forwarded-host') ?? headers.get('host')
|
|
48
|
+
const detectedProtocol =
|
|
49
|
+
headers.get('x-forwarded-proto') ?? protocol ?? 'https'
|
|
50
|
+
|
|
51
|
+
url = new URL(`${detectedProtocol}://${detectedHost}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// remove trailing slash
|
|
55
|
+
const sanitizedUrl = url.toString().replace(/\/$/, '')
|
|
56
|
+
|
|
57
|
+
if (basePath) {
|
|
58
|
+
// remove leading and trailing slash
|
|
59
|
+
const sanitizedBasePath = basePath?.replace(/(^\/|\/$)/g, '') ?? ''
|
|
60
|
+
return new URL(`${sanitizedUrl}/${sanitizedBasePath}/${action}`)
|
|
61
|
+
}
|
|
62
|
+
return new URL(`${sanitizedUrl}/${action}`)
|
|
63
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { AdapterError } from '@coursebuilder/core/errors'
|
|
2
|
+
import {
|
|
3
|
+
CallbacksOptions,
|
|
4
|
+
InternalOptions,
|
|
5
|
+
RequestInternal,
|
|
6
|
+
} from '@coursebuilder/core/types'
|
|
7
|
+
|
|
8
|
+
import { CourseBuilderConfig } from '../http'
|
|
9
|
+
import * as cookie from './cookie.js'
|
|
10
|
+
import { logger, LoggerInstance } from './logger'
|
|
11
|
+
import { merge } from './merge'
|
|
12
|
+
import parseProviders from './providers'
|
|
13
|
+
|
|
14
|
+
interface InitParams {
|
|
15
|
+
url: URL
|
|
16
|
+
courseBuilderOptions: CourseBuilderConfig
|
|
17
|
+
providerId?: string
|
|
18
|
+
action: InternalOptions['action']
|
|
19
|
+
isPost: boolean
|
|
20
|
+
cookies: RequestInternal['cookies']
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function init({
|
|
24
|
+
url,
|
|
25
|
+
courseBuilderOptions,
|
|
26
|
+
providerId,
|
|
27
|
+
action,
|
|
28
|
+
isPost,
|
|
29
|
+
cookies: reqCookies,
|
|
30
|
+
}: InitParams): Promise<{
|
|
31
|
+
options: InternalOptions
|
|
32
|
+
cookies: cookie.Cookie[]
|
|
33
|
+
}> {
|
|
34
|
+
const { providers, provider } = parseProviders({
|
|
35
|
+
providers: courseBuilderOptions.providers,
|
|
36
|
+
url,
|
|
37
|
+
providerId,
|
|
38
|
+
options: courseBuilderOptions,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const maxAge = 30 * 24 * 60 * 60
|
|
42
|
+
|
|
43
|
+
const options: InternalOptions = {
|
|
44
|
+
debug: false,
|
|
45
|
+
pages: {},
|
|
46
|
+
...courseBuilderOptions,
|
|
47
|
+
url,
|
|
48
|
+
action,
|
|
49
|
+
// @ts-expect-errors
|
|
50
|
+
provider,
|
|
51
|
+
cookies: merge(
|
|
52
|
+
{},
|
|
53
|
+
cookie.defaultCookies(
|
|
54
|
+
courseBuilderOptions.useSecureCookies ?? url.protocol === 'https:',
|
|
55
|
+
),
|
|
56
|
+
courseBuilderOptions.cookies || {},
|
|
57
|
+
reqCookies,
|
|
58
|
+
),
|
|
59
|
+
providers,
|
|
60
|
+
adapter: adapterErrorHandler(courseBuilderOptions.adapter, logger),
|
|
61
|
+
callbacks: { ...defaultCallbacks, ...courseBuilderOptions.callbacks },
|
|
62
|
+
logger,
|
|
63
|
+
getCurrentUser: courseBuilderOptions.getCurrentUser,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Convert request cookies to Cookie[] format for actions
|
|
67
|
+
const cookies: cookie.Cookie[] = Object.entries(reqCookies || {})
|
|
68
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === 'string')
|
|
69
|
+
.map(([name, value]) => ({
|
|
70
|
+
name,
|
|
71
|
+
value,
|
|
72
|
+
options: {},
|
|
73
|
+
}))
|
|
74
|
+
|
|
75
|
+
logger.debug('init.request-cookies', {
|
|
76
|
+
rawCookieKeys: Object.keys(reqCookies || {}),
|
|
77
|
+
convertedCount: cookies.length,
|
|
78
|
+
hasSl_ref: cookies.some((c) => c.name === 'sl_ref'),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return { options, cookies }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type Method = (...args: any[]) => Promise<any>
|
|
85
|
+
|
|
86
|
+
export const defaultCallbacks: CallbacksOptions = {
|
|
87
|
+
session(payload) {
|
|
88
|
+
return payload
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function adapterErrorHandler(
|
|
93
|
+
adapter: CourseBuilderConfig['adapter'],
|
|
94
|
+
logger: LoggerInstance,
|
|
95
|
+
) {
|
|
96
|
+
if (!adapter) return
|
|
97
|
+
|
|
98
|
+
return Object.keys(adapter).reduce<any>((acc, name) => {
|
|
99
|
+
acc[name] = async (...args: any[]) => {
|
|
100
|
+
try {
|
|
101
|
+
logger.debug(`adapter_${name}`, { args })
|
|
102
|
+
const method: Method = adapter[name as keyof Method]
|
|
103
|
+
return await method(...args)
|
|
104
|
+
} catch (e) {
|
|
105
|
+
const error = new AdapterError(e as Error)
|
|
106
|
+
logger.error(error)
|
|
107
|
+
throw error
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return acc
|
|
111
|
+
}, {})
|
|
112
|
+
}
|