-
Notifications
You must be signed in to change notification settings - Fork 2.7k
/
Copy pathunroll.ts
385 lines (338 loc) · 10.3 KB
/
unroll.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
import * as YAML from "yaml";
import { PlatformClient, Registry } from "../interfaces/index.js";
import { encodeSecretLocation } from "../interfaces/SecretResult.js";
import {
decodeFQSN,
decodeFullSlug,
encodeFQSN,
encodePackageSlug,
FQSN,
FullSlug,
PackageSlug,
} from "../interfaces/slugs.js";
import {
AssistantUnrolled,
assistantUnrolledSchema,
Block,
blockSchema,
ConfigYaml,
configYamlSchema,
} from "../schemas/index.js";
import { useProxyForUnrenderedSecrets } from "./clientRender.js";
export function parseConfigYaml(configYaml: string): ConfigYaml {
try {
const parsed = YAML.parse(configYaml);
const result = configYamlSchema.safeParse(parsed);
if (result.success) {
return result.data;
}
throw new Error(
result.error.errors
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join(""),
);
} catch (e) {
console.log("Failed to parse rolled assistant:", configYaml);
throw new Error(
`Failed to parse assistant:\n${e instanceof Error ? e.message : e}`,
);
}
}
export function parseAssistantUnrolled(configYaml: string): AssistantUnrolled {
try {
const parsed = YAML.parse(configYaml);
const result = assistantUnrolledSchema.parse(parsed);
return result;
} catch (e: any) {
throw new Error(
`Failed to parse unrolled assistant: ${e.message}\n\n${configYaml}`,
);
}
}
export function parseBlock(configYaml: string): Block {
try {
const parsed = YAML.parse(configYaml);
const result = blockSchema.parse(parsed);
return result;
} catch (e: any) {
throw new Error(`Failed to parse block: ${e.message}`);
}
}
const TEMPLATE_VAR_REGEX = /\${{[\s]*([^}\s]+)[\s]*}}/g;
export function getTemplateVariables(templatedYaml: string): string[] {
const variables = new Set<string>();
const matches = templatedYaml.matchAll(TEMPLATE_VAR_REGEX);
for (const match of matches) {
variables.add(match[1]);
}
return Array.from(variables);
}
export function fillTemplateVariables(
templatedYaml: string,
data: { [key: string]: string },
): string {
return templatedYaml.replace(TEMPLATE_VAR_REGEX, (match, variableName) => {
// Inject data
if (variableName in data) {
return data[variableName];
}
// If variable doesn't exist, return the original expression
return match;
});
}
export interface TemplateData {
inputs: Record<string, string> | undefined;
secrets: Record<string, string> | undefined;
continue: {};
}
function flattenTemplateData(
templateData: TemplateData,
): Record<string, string> {
const flattened: Record<string, string> = {};
if (templateData.inputs) {
for (const [key, value] of Object.entries(templateData.inputs)) {
flattened[`inputs.${key}`] = value;
}
}
if (templateData.secrets) {
for (const [key, value] of Object.entries(templateData.secrets)) {
flattened[`secrets.${key}`] = value;
}
}
return flattened;
}
function secretToFQSNMap(
secretNames: string[],
parentPackages: PackageSlug[],
): Record<string, string> {
const map: Record<string, string> = {};
for (const secret of secretNames) {
const parentSlugs = parentPackages.map(encodePackageSlug);
const parts = [...parentSlugs, secret];
const fqsn = parts.join("/");
map[secret] = `\${{ secrets.${fqsn} }}`;
}
return map;
}
function extractFQSNMap(
rawContent: string,
parentPackages: PackageSlug[],
): Record<string, string> {
const templateVars = getTemplateVariables(rawContent);
const secrets = templateVars
.filter((v) => v.startsWith("secrets."))
.map((v) => v.replace("secrets.", ""));
return secretToFQSNMap(secrets, parentPackages);
}
/**
* All template vars are already FQSNs, here we just resolve them to either locations or values
*/
async function extractRenderedSecretsMap(
rawContent: string,
platformClient: PlatformClient,
): Promise<Record<string, string>> {
// Get all template variables
const templateVars = getTemplateVariables(rawContent);
const secrets = templateVars
.filter((v) => v.startsWith("secrets."))
.map((v) => v.replace("secrets.", ""));
const fqsns: FQSN[] = secrets.map(decodeFQSN);
// FQSN -> SecretResult
const secretResults = await platformClient.resolveFQSNs(fqsns);
const map: Record<string, string> = {};
for (const secretResult of secretResults) {
if (!secretResult) {
continue;
}
// User secrets are rendered
if ("value" in secretResult) {
map[encodeFQSN(secretResult.fqsn)] = secretResult.value;
} else {
// Other secrets are rendered as secret locations and then converted to proxy types later
map[encodeFQSN(secretResult.fqsn)] =
`\${{ secrets.${encodeSecretLocation(secretResult.secretLocation)} }}`;
}
}
return map;
}
export interface DoNotRenderSecretsUnrollAssistantOptions {
renderSecrets: false;
}
export interface RenderSecretsUnrollAssistantOptions {
renderSecrets: true;
orgScopeId: string | null;
currentUserSlug: string;
platformClient: PlatformClient;
onPremProxyUrl: string | null;
}
export type UnrollAssistantOptions =
| DoNotRenderSecretsUnrollAssistantOptions
| RenderSecretsUnrollAssistantOptions;
export async function unrollAssistant(
fullSlug: string,
registry: Registry,
options: UnrollAssistantOptions,
): Promise<AssistantUnrolled> {
const assistantSlug = decodeFullSlug(fullSlug);
// Request the content from the registry
const rawContent = await registry.getContent(assistantSlug);
return unrollAssistantFromContent(
assistantSlug,
rawContent,
registry,
options,
);
}
function renderTemplateData(
rawYaml: string,
templateData: Partial<TemplateData>,
): string {
const fullTemplateData: TemplateData = {
inputs: {},
secrets: {},
continue: {},
...templateData,
};
const templatedYaml = fillTemplateVariables(
rawYaml,
flattenTemplateData(fullTemplateData),
);
return templatedYaml;
}
export async function unrollAssistantFromContent(
assistantSlug: FullSlug,
rawYaml: string,
registry: Registry,
options: UnrollAssistantOptions,
): Promise<AssistantUnrolled> {
// Parse string to Zod-validated YAML
let parsedYaml = parseConfigYaml(rawYaml);
// Unroll blocks and convert their secrets to FQSNs
const unrolledAssistant = await unrollBlocks(parsedYaml, registry);
// Back to a string so we can fill in template variables
const rawUnrolledYaml = YAML.stringify(unrolledAssistant);
// Convert all of the template variables to FQSNs
// Secrets from the block will have the assistant slug prepended to the FQSN
const templatedYaml = renderTemplateData(rawUnrolledYaml, {
secrets: extractFQSNMap(rawUnrolledYaml, [assistantSlug]),
});
if (!options.renderSecrets) {
return parseAssistantUnrolled(templatedYaml);
}
// Render secret values/locations for client
const secrets = await extractRenderedSecretsMap(
templatedYaml,
options.platformClient,
);
const renderedYaml = renderTemplateData(templatedYaml, {
secrets,
});
// Parse again and replace models with proxy versions where secrets weren't rendered
const finalConfig = useProxyForUnrenderedSecrets(
parseAssistantUnrolled(renderedYaml),
assistantSlug,
options.orgScopeId,
options.onPremProxyUrl,
);
return finalConfig;
}
export async function unrollBlocks(
assistant: ConfigYaml,
registry: Registry,
): Promise<AssistantUnrolled> {
const unrolledAssistant: AssistantUnrolled = {
name: assistant.name,
version: assistant.version,
};
const sections: (keyof Omit<
ConfigYaml,
"name" | "version" | "rules" | "schema"
>)[] = ["models", "context", "data", "mcpServers", "prompts", "docs"];
// For each section, replace "uses/with" blocks with the real thing
for (const section of sections) {
if (assistant[section]) {
const sectionBlocks: any[] = [];
for (const unrolledBlock of assistant[section]) {
// "uses/with" block
if ("uses" in unrolledBlock) {
const blockConfigYaml = await resolveBlock(
decodeFullSlug(unrolledBlock.uses),
unrolledBlock.with,
registry,
);
const block = blockConfigYaml[section]?.[0];
if (block) {
sectionBlocks.push(
mergeOverrides(block, unrolledBlock.override ?? {}),
);
}
} else {
// Normal block
sectionBlocks.push(unrolledBlock);
}
}
unrolledAssistant[section] = sectionBlocks;
}
}
// Rules are a bit different because they're just strings, so handle separately
if (assistant.rules) {
const rules: string[] = [];
for (const rule of assistant.rules) {
if (typeof rule === "string") {
rules.push(rule);
} else {
const blockConfigYaml = await resolveBlock(
decodeFullSlug(rule.uses),
rule.with,
registry,
);
const block = blockConfigYaml.rules?.[0];
if (block) {
rules.push(block);
}
}
}
unrolledAssistant.rules = rules;
}
return unrolledAssistant;
}
export async function resolveBlock(
fullSlug: FullSlug,
inputs: Record<string, string> | undefined,
registry: Registry,
): Promise<AssistantUnrolled> {
// Retrieve block raw yaml
const rawYaml = await registry.getContent(fullSlug);
// Convert any input secrets to FQSNs (they get FQSNs as if they are in the block. This is so that we know when to use models add-on / free trial secrets)
const renderedInputs = inputsToFQSNs(inputs || {}, fullSlug);
// Render template variables
const templatedYaml = renderTemplateData(rawYaml, {
inputs: renderedInputs,
secrets: extractFQSNMap(rawYaml, [fullSlug]),
});
const parsedYaml = parseBlock(templatedYaml);
return parsedYaml;
}
function inputsToFQSNs(
inputs: Record<string, string>,
blockSlug: PackageSlug,
): Record<string, string> {
const renderedInputs: Record<string, string> = {};
for (const [key, value] of Object.entries(inputs)) {
renderedInputs[key] = renderTemplateData(value, {
secrets: extractFQSNMap(value, [blockSlug]),
});
}
return renderedInputs;
}
export function mergeOverrides<T extends Record<string, any>>(
block: T,
overrides: Partial<T>,
): T {
for (const key in overrides) {
if (overrides.hasOwnProperty(key)) {
block[key] = overrides[key]!;
}
}
return block;
}