@kubuild/ai 0.3.0
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/chunk-27AY2KBI.js +500 -0
- package/dist/chunk-27AY2KBI.js.map +1 -0
- package/dist/chunk-JY4NHD3Q.js +227 -0
- package/dist/chunk-JY4NHD3Q.js.map +1 -0
- package/dist/chunk-UH24LHRY.js +443 -0
- package/dist/chunk-UH24LHRY.js.map +1 -0
- package/dist/client/ai-client.d.ts +59 -0
- package/dist/client/ai-client.d.ts.map +1 -0
- package/dist/client/index.cjs +254 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +9 -0
- package/dist/client/index.js.map +1 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/normalizer.d.ts +9 -0
- package/dist/core/normalizer.d.ts.map +1 -0
- package/dist/core/prompt-compiler.d.ts +10 -0
- package/dist/core/prompt-compiler.d.ts.map +1 -0
- package/dist/index.cjs +468 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +584 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +336 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/use-ai-chat.d.ts +63 -0
- package/dist/react/use-ai-chat.d.ts.map +1 -0
- package/dist/react/use-ai-generator.d.ts +19 -0
- package/dist/react/use-ai-generator.d.ts.map +1 -0
- package/dist/server/adapters/anthropic.d.ts +30 -0
- package/dist/server/adapters/anthropic.d.ts.map +1 -0
- package/dist/server/adapters/base.d.ts +2 -0
- package/dist/server/adapters/base.d.ts.map +1 -0
- package/dist/server/adapters/custom.d.ts +18 -0
- package/dist/server/adapters/custom.d.ts.map +1 -0
- package/dist/server/adapters/gemini.d.ts +22 -0
- package/dist/server/adapters/gemini.d.ts.map +1 -0
- package/dist/server/adapters/index.cjs +529 -0
- package/dist/server/adapters/index.cjs.map +1 -0
- package/dist/server/adapters/index.d.ts +6 -0
- package/dist/server/adapters/index.d.ts.map +1 -0
- package/dist/server/adapters/index.js +13 -0
- package/dist/server/adapters/index.js.map +1 -0
- package/dist/server/adapters/openai.d.ts +26 -0
- package/dist/server/adapters/openai.d.ts.map +1 -0
- package/dist/server/adapters/stream-utils.d.ts +12 -0
- package/dist/server/adapters/stream-utils.d.ts.map +1 -0
- package/dist/server/engine.d.ts +59 -0
- package/dist/server/engine.d.ts.map +1 -0
- package/dist/server/handler.d.ts +66 -0
- package/dist/server/handler.d.ts.map +1 -0
- package/dist/server/index.cjs +1824 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +890 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types.d.ts +184 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,1824 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/server/index.ts
|
|
21
|
+
var server_exports = {};
|
|
22
|
+
__export(server_exports, {
|
|
23
|
+
AnthropicAdapter: () => AnthropicAdapter,
|
|
24
|
+
CustomHttpAdapter: () => CustomHttpAdapter,
|
|
25
|
+
GeminiAdapter: () => GeminiAdapter,
|
|
26
|
+
KubuildAiEngine: () => KubuildAiEngine,
|
|
27
|
+
OpenAiAdapter: () => OpenAiAdapter,
|
|
28
|
+
createAiHandler: () => createAiHandler,
|
|
29
|
+
processAiRequest: () => processAiRequest
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(server_exports);
|
|
32
|
+
|
|
33
|
+
// src/server/engine.ts
|
|
34
|
+
var import_schema2 = require("@kubuild/schema");
|
|
35
|
+
|
|
36
|
+
// src/core/prompt-compiler.ts
|
|
37
|
+
function compileComponentCatalog(registry) {
|
|
38
|
+
if (!registry) return [];
|
|
39
|
+
const definitions = registry.list();
|
|
40
|
+
return definitions.map((def) => {
|
|
41
|
+
const props = [];
|
|
42
|
+
if (def.propFields && Array.isArray(def.propFields)) {
|
|
43
|
+
for (const field of def.propFields) {
|
|
44
|
+
props.push({
|
|
45
|
+
name: field.name,
|
|
46
|
+
type: field.type || "string",
|
|
47
|
+
defaultValue: field.defaultValue,
|
|
48
|
+
options: field.options?.map((o) => o.value),
|
|
49
|
+
description: field.description
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
type: def.type,
|
|
55
|
+
label: def.label || def.type,
|
|
56
|
+
category: def.category || "custom",
|
|
57
|
+
description: def.description,
|
|
58
|
+
acceptsChildren: def.acceptsChildren ?? false,
|
|
59
|
+
allowedChildren: def.allowedChildren,
|
|
60
|
+
disallowedParents: def.disallowedParents,
|
|
61
|
+
defaultProps: def.defaultProps,
|
|
62
|
+
props: props.length > 0 ? props : void 0
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function buildSystemPrompt(options) {
|
|
67
|
+
const { catalog, mode, prefix, stylePreference } = options;
|
|
68
|
+
const catalogSummary = catalog.map((c) => {
|
|
69
|
+
let desc = `- **${c.type}** (${c.category}): ${c.label}`;
|
|
70
|
+
if (c.description) desc += ` - ${c.description}`;
|
|
71
|
+
desc += ` | acceptsChildren: ${c.acceptsChildren}`;
|
|
72
|
+
if (c.allowedChildren && c.allowedChildren.length > 0) {
|
|
73
|
+
desc += ` | allowedChildren: [${c.allowedChildren.join(", ")}]`;
|
|
74
|
+
}
|
|
75
|
+
if (c.props && c.props.length > 0) {
|
|
76
|
+
const propList = c.props.map((p) => {
|
|
77
|
+
let pStr = `${p.name} (${p.type})`;
|
|
78
|
+
if (p.options) pStr += `[${p.options.map(String).join("|")}]`;
|
|
79
|
+
return pStr;
|
|
80
|
+
}).join(", ");
|
|
81
|
+
desc += ` | props: { ${propList} }`;
|
|
82
|
+
}
|
|
83
|
+
return desc;
|
|
84
|
+
}).join("\n");
|
|
85
|
+
let modeInstructions = "";
|
|
86
|
+
if (mode === "full-page") {
|
|
87
|
+
modeInstructions = `
|
|
88
|
+
You must generate a complete, valid KUBUILD PageDocument JSON with this exact structure:
|
|
89
|
+
{
|
|
90
|
+
"schema": "stora.page",
|
|
91
|
+
"version": "1.0.0",
|
|
92
|
+
"metadata": {
|
|
93
|
+
"title": "<Page Title>",
|
|
94
|
+
"description": "<Page Description>",
|
|
95
|
+
"tags": ["<tag1>", "<tag2>"],
|
|
96
|
+
"category": "<category>"
|
|
97
|
+
},
|
|
98
|
+
"document": {
|
|
99
|
+
"id": "root-page",
|
|
100
|
+
"type": "page",
|
|
101
|
+
"styles": {
|
|
102
|
+
"base": {
|
|
103
|
+
"backgroundColor": "#ffffff",
|
|
104
|
+
"fontFamily": "Inter, system-ui, sans-serif",
|
|
105
|
+
"color": "#111827",
|
|
106
|
+
"minHeight": "100vh"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"children": [
|
|
110
|
+
// Sections here...
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
`;
|
|
115
|
+
} else if (mode === "section") {
|
|
116
|
+
modeInstructions = `
|
|
117
|
+
You must generate a single valid KUBUILD section Node JSON with this structure:
|
|
118
|
+
{
|
|
119
|
+
"id": "section_<unique_suffix>",
|
|
120
|
+
"type": "section",
|
|
121
|
+
"styles": {
|
|
122
|
+
"base": {
|
|
123
|
+
"paddingTop": "64px",
|
|
124
|
+
"paddingBottom": "64px",
|
|
125
|
+
"paddingLeft": "24px",
|
|
126
|
+
"paddingRight": "24px"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"children": [
|
|
130
|
+
// Container and child nodes...
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
`;
|
|
134
|
+
} else if (mode === "refactor") {
|
|
135
|
+
modeInstructions = `
|
|
136
|
+
You must refactor the provided KUBUILD Node based on the user's instructions.
|
|
137
|
+
Return ONLY the updated Node JSON while maintaining structural integrity.
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
const styleGuide = stylePreference ? `
|
|
141
|
+
User Style Preference: "${stylePreference}". Follow this visual aesthetic closely.` : "\nStyle with modern, clean, high-conversion design patterns (accessible contrast, harmonious spacing, beautiful typography, subtle shadows/borders).";
|
|
142
|
+
return `${prefix ? prefix + "\n\n" : ""}You are an expert web designer and KUBUILD architecture generator.
|
|
143
|
+
KUBUILD is a component-driven visual website builder. Your job is to output pure, valid JSON (and NOTHING ELSE) matching the KUBUILD schema and the registered component definitions.
|
|
144
|
+
|
|
145
|
+
### RULES & CONSTRAINTS:
|
|
146
|
+
1. Output MUST be valid JSON only. No markdown fences, no explanatory text, no HTML tags outside html-embed.
|
|
147
|
+
2. Only use component types listed in the Component Catalog below. Do NOT invent new component types.
|
|
148
|
+
3. Obey \`acceptsChildren\` and \`allowedChildren\`:
|
|
149
|
+
- Layout hierarchy: \`page\` -> \`section\` -> \`container\` or \`columns\` -> content components (\`heading\`, \`paragraph\`, \`button\`, \`image\`, \`form\`, etc.).
|
|
150
|
+
- Leaf components (e.g. \`button\`, \`input\`, \`heading\`, \`text\`, \`badge\`) must have \`acceptsChildren: false\` and empty or omitted \`children\`.
|
|
151
|
+
4. Every node MUST have a unique string \`id\` (e.g. "hero-section", "hero-title", "cta-btn").
|
|
152
|
+
5. Responsive & Interactive State Styling:
|
|
153
|
+
- Use the \`styles\` object with breakpoint keys: \`base\` (all viewports/desktop), and optionally \`tablet\` or \`mobile\`.
|
|
154
|
+
- In \`base\`, \`tablet\`, and \`mobile\`, values MUST be CSS primitives (strings or numbers, e.g. "backgroundColor": "#3b82f6"). NEVER put nested objects or ":hover" inside "base"!
|
|
155
|
+
- For hover, active, or interactive pseudo-classes, use \`styles.states\` with pseudo-class keys (e.g. \`":hover"\`), like:
|
|
156
|
+
"styles": {
|
|
157
|
+
"base": { "backgroundColor": "#3b82f6", "color": "#ffffff" },
|
|
158
|
+
"states": {
|
|
159
|
+
":hover": { "backgroundColor": "#2563eb" }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
6. NEVER include dangerous scripts, \`javascript:\` URIs, or malicious payload attributes.
|
|
163
|
+
${styleGuide}
|
|
164
|
+
|
|
165
|
+
### OUTPUT SPECIFICATION:
|
|
166
|
+
${modeInstructions}
|
|
167
|
+
|
|
168
|
+
### REGISTERED COMPONENT CATALOG:
|
|
169
|
+
${catalogSummary || "(Standard core components: page, section, container, columns, heading, paragraph, text, button, input, textarea, select, image, video, icon, badge, code-block)"}
|
|
170
|
+
`;
|
|
171
|
+
}
|
|
172
|
+
function buildJsonSchemaForMode(mode) {
|
|
173
|
+
const nodeSchema = {
|
|
174
|
+
type: "object",
|
|
175
|
+
required: ["id", "type"],
|
|
176
|
+
properties: {
|
|
177
|
+
id: { type: "string" },
|
|
178
|
+
type: { type: "string" },
|
|
179
|
+
props: { type: "object" },
|
|
180
|
+
styles: {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: {
|
|
183
|
+
base: { type: "object" },
|
|
184
|
+
tablet: { type: "object" },
|
|
185
|
+
mobile: { type: "object" },
|
|
186
|
+
states: { type: "object" }
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
children: {
|
|
190
|
+
type: "array",
|
|
191
|
+
items: { $ref: "#" }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
if (mode === "full-page") {
|
|
196
|
+
return {
|
|
197
|
+
type: "object",
|
|
198
|
+
required: ["schema", "version", "document"],
|
|
199
|
+
properties: {
|
|
200
|
+
schema: { type: "string", const: "stora.page" },
|
|
201
|
+
version: { type: "string", const: "1.0.0" },
|
|
202
|
+
metadata: {
|
|
203
|
+
type: "object",
|
|
204
|
+
properties: {
|
|
205
|
+
title: { type: "string" },
|
|
206
|
+
description: { type: "string" },
|
|
207
|
+
tags: { type: "array", items: { type: "string" } },
|
|
208
|
+
category: { type: "string" }
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
document: nodeSchema
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return nodeSchema;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/core/normalizer.ts
|
|
219
|
+
var import_schema = require("@kubuild/schema");
|
|
220
|
+
var import_core = require("@kubuild/core");
|
|
221
|
+
function extractJsonFromResponse(raw) {
|
|
222
|
+
let cleaned = raw.trim();
|
|
223
|
+
if (cleaned.startsWith("```")) {
|
|
224
|
+
const firstNewline = cleaned.indexOf("\n");
|
|
225
|
+
if (firstNewline !== -1) {
|
|
226
|
+
cleaned = cleaned.slice(firstNewline + 1);
|
|
227
|
+
}
|
|
228
|
+
if (cleaned.endsWith("```")) {
|
|
229
|
+
cleaned = cleaned.slice(0, cleaned.length - 3).trim();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const firstBrace = cleaned.indexOf("{");
|
|
233
|
+
const lastBrace = cleaned.lastIndexOf("}");
|
|
234
|
+
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
|
235
|
+
cleaned = cleaned.slice(firstBrace, lastBrace + 1);
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(cleaned);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
241
|
+
throw new Error(`Failed to parse AI response as JSON: ${msg}. Response was: ${raw.slice(0, 200)}...`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function normalizeStyles(rawStyles) {
|
|
245
|
+
if (!rawStyles || typeof rawStyles !== "object" || Array.isArray(rawStyles)) {
|
|
246
|
+
return void 0;
|
|
247
|
+
}
|
|
248
|
+
const raw = rawStyles;
|
|
249
|
+
const result = {};
|
|
250
|
+
const states = {};
|
|
251
|
+
if (raw.states && typeof raw.states === "object" && !Array.isArray(raw.states)) {
|
|
252
|
+
for (const [stateKey, stateVal] of Object.entries(raw.states)) {
|
|
253
|
+
if (stateVal && typeof stateVal === "object" && !Array.isArray(stateVal)) {
|
|
254
|
+
states[stateKey] = { ...stateVal };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const knownBreakpoints = ["base", "desktop", "tablet", "mobile"];
|
|
259
|
+
const hasBreakpoint = Object.keys(raw).some((k) => knownBreakpoints.includes(k));
|
|
260
|
+
if (!hasBreakpoint) {
|
|
261
|
+
const baseStyles = {};
|
|
262
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
263
|
+
if (key === "states") continue;
|
|
264
|
+
if (key.startsWith(":") || key === "hover" || key === "focus" || key === "active") {
|
|
265
|
+
const stateKey = key.startsWith(":") ? key : `:${key}`;
|
|
266
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
267
|
+
states[stateKey] = { ...states[stateKey] || {}, ...value };
|
|
268
|
+
}
|
|
269
|
+
} else if (typeof value !== "object" || value === null) {
|
|
270
|
+
baseStyles[key] = value;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (Object.keys(baseStyles).length > 0) {
|
|
274
|
+
result.base = baseStyles;
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
for (const bp of knownBreakpoints) {
|
|
278
|
+
const bpVal = raw[bp];
|
|
279
|
+
if (bpVal && typeof bpVal === "object" && !Array.isArray(bpVal)) {
|
|
280
|
+
const cleanBp = {};
|
|
281
|
+
for (const [key, value] of Object.entries(bpVal)) {
|
|
282
|
+
if (key.startsWith(":") || key === "hover" || key === "focus" || key === "active") {
|
|
283
|
+
const stateKey = key.startsWith(":") ? key : `:${key}`;
|
|
284
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
285
|
+
states[stateKey] = { ...states[stateKey] || {}, ...value };
|
|
286
|
+
}
|
|
287
|
+
} else if (typeof value !== "object" || value === null) {
|
|
288
|
+
cleanBp[key] = value;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (Object.keys(cleanBp).length > 0) {
|
|
292
|
+
result[bp] = cleanBp;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
297
|
+
if (knownBreakpoints.includes(key) || key === "states") continue;
|
|
298
|
+
if (key.startsWith(":") || key === "hover" || key === "focus" || key === "active") {
|
|
299
|
+
const stateKey = key.startsWith(":") ? key : `:${key}`;
|
|
300
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
301
|
+
states[stateKey] = { ...states[stateKey] || {}, ...value };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const cleanStates = {};
|
|
307
|
+
for (const [stateKey, stateDef] of Object.entries(states)) {
|
|
308
|
+
const cleanDef = {};
|
|
309
|
+
for (const [propKey, propVal] of Object.entries(stateDef)) {
|
|
310
|
+
if (typeof propVal !== "object" || propVal === null) {
|
|
311
|
+
cleanDef[propKey] = propVal;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (Object.keys(cleanDef).length > 0) {
|
|
315
|
+
cleanStates[stateKey] = cleanDef;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (Object.keys(cleanStates).length > 0) {
|
|
319
|
+
result.states = cleanStates;
|
|
320
|
+
}
|
|
321
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
322
|
+
}
|
|
323
|
+
function normalizeNodeTree(node, usedIds = /* @__PURE__ */ new Set(), depth = 0) {
|
|
324
|
+
if (!node || typeof node !== "object") {
|
|
325
|
+
throw new Error(`Invalid node at depth ${depth}: must be an object`);
|
|
326
|
+
}
|
|
327
|
+
const raw = node;
|
|
328
|
+
const type = typeof raw.type === "string" && raw.type.trim() ? raw.type.trim() : "container";
|
|
329
|
+
let id = typeof raw.id === "string" && raw.id.trim() ? raw.id.trim() : "";
|
|
330
|
+
if (!id || usedIds.has(id)) {
|
|
331
|
+
let counter = 1;
|
|
332
|
+
let candidate = `${type}-${counter}`;
|
|
333
|
+
while (usedIds.has(candidate)) {
|
|
334
|
+
counter++;
|
|
335
|
+
candidate = `${type}-${counter}`;
|
|
336
|
+
}
|
|
337
|
+
id = candidate;
|
|
338
|
+
}
|
|
339
|
+
usedIds.add(id);
|
|
340
|
+
const props = raw.props && typeof raw.props === "object" && !Array.isArray(raw.props) ? { ...raw.props } : {};
|
|
341
|
+
if (type === "heading" && typeof props.level === "string") {
|
|
342
|
+
const parsedLevel = parseInt(props.level, 10);
|
|
343
|
+
if (!isNaN(parsedLevel) && parsedLevel >= 1 && parsedLevel <= 6) {
|
|
344
|
+
props.level = parsedLevel;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const styles = normalizeStyles(raw.styles);
|
|
348
|
+
let children = void 0;
|
|
349
|
+
if (Array.isArray(raw.children)) {
|
|
350
|
+
children = raw.children.filter((c) => c && typeof c === "object").map((c) => normalizeNodeTree(c, usedIds, depth + 1));
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
id,
|
|
354
|
+
type,
|
|
355
|
+
props: Object.keys(props).length > 0 ? props : void 0,
|
|
356
|
+
styles,
|
|
357
|
+
children
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function normalizeAndValidatePageDocument(rawJson, securityLimits) {
|
|
361
|
+
if (!rawJson || typeof rawJson !== "object") {
|
|
362
|
+
throw new Error("AI output must be a JSON object");
|
|
363
|
+
}
|
|
364
|
+
const obj = rawJson;
|
|
365
|
+
let docObject;
|
|
366
|
+
if (obj.type === "page" && !obj.document) {
|
|
367
|
+
docObject = {
|
|
368
|
+
schema: import_schema.SCHEMA_NAME,
|
|
369
|
+
version: import_schema.CURRENT_SCHEMA_VERSION,
|
|
370
|
+
metadata: {
|
|
371
|
+
title: typeof obj.props === "object" && obj.props !== null && obj.props.title || "AI Generated Page",
|
|
372
|
+
description: "Generated by KUBUILD AI"
|
|
373
|
+
},
|
|
374
|
+
document: obj
|
|
375
|
+
};
|
|
376
|
+
} else {
|
|
377
|
+
docObject = {
|
|
378
|
+
schema: import_schema.SCHEMA_NAME,
|
|
379
|
+
version: typeof obj.version === "string" ? obj.version : import_schema.CURRENT_SCHEMA_VERSION,
|
|
380
|
+
metadata: typeof obj.metadata === "object" && obj.metadata !== null ? obj.metadata : { title: "AI Generated Page", description: "Generated by KUBUILD AI" },
|
|
381
|
+
document: obj.document || obj
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
385
|
+
const normalizedRoot = normalizeNodeTree(docObject.document, usedIds);
|
|
386
|
+
if (normalizedRoot.type !== "page") {
|
|
387
|
+
normalizedRoot.type = "page";
|
|
388
|
+
}
|
|
389
|
+
const parsedMetadata = import_schema.DocumentMetadataSchema.parse({
|
|
390
|
+
title: "AI Generated Page",
|
|
391
|
+
description: "Generated by KUBUILD AI",
|
|
392
|
+
author: "KUBUILD AI",
|
|
393
|
+
tags: ["ai-generated"],
|
|
394
|
+
category: "general",
|
|
395
|
+
version: "1.0.0",
|
|
396
|
+
...typeof docObject.metadata === "object" && docObject.metadata !== null ? docObject.metadata : {}
|
|
397
|
+
});
|
|
398
|
+
const finalDocument = {
|
|
399
|
+
schema: import_schema.SCHEMA_NAME,
|
|
400
|
+
version: import_schema.CURRENT_SCHEMA_VERSION,
|
|
401
|
+
metadata: parsedMetadata,
|
|
402
|
+
document: normalizedRoot
|
|
403
|
+
};
|
|
404
|
+
const parsed = import_schema.PageDocumentSchema.safeParse(finalDocument);
|
|
405
|
+
if (!parsed.success) {
|
|
406
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
407
|
+
throw new Error(`Document schema validation failed: ${issues}`);
|
|
408
|
+
}
|
|
409
|
+
const validationResult = (0, import_core.validateDocument)(finalDocument, { securityLimits });
|
|
410
|
+
if (!validationResult.valid) {
|
|
411
|
+
const errorMsgs = validationResult.errors.map((e) => `${e.code}: ${e.message} (${e.path})`).join("; ");
|
|
412
|
+
throw new Error(`Document validation failed: ${errorMsgs}`);
|
|
413
|
+
}
|
|
414
|
+
return parsed.data;
|
|
415
|
+
}
|
|
416
|
+
function normalizeAndValidateSectionNode(rawJson, securityLimits) {
|
|
417
|
+
if (!rawJson || typeof rawJson !== "object") {
|
|
418
|
+
throw new Error("AI output must be a JSON object");
|
|
419
|
+
}
|
|
420
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
421
|
+
const normalized = normalizeNodeTree(rawJson, usedIds);
|
|
422
|
+
if (normalized.type !== "section") {
|
|
423
|
+
normalized.type = "section";
|
|
424
|
+
}
|
|
425
|
+
const parsed = import_schema.NodeSchema.safeParse(normalized);
|
|
426
|
+
if (!parsed.success) {
|
|
427
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
428
|
+
throw new Error(`Section node schema validation failed: ${issues}`);
|
|
429
|
+
}
|
|
430
|
+
const secCheck = (0, import_core.validateDocumentSecurity)({ document: normalized }, securityLimits);
|
|
431
|
+
if (!secCheck.safe) {
|
|
432
|
+
const issues = secCheck.errors.map((e) => `${e.code}: ${e.message}`).join("; ");
|
|
433
|
+
throw new Error(`Section security check failed: ${issues}`);
|
|
434
|
+
}
|
|
435
|
+
return parsed.data;
|
|
436
|
+
}
|
|
437
|
+
function normalizeAndValidateRefactoredNode(rawJson, originalNode, securityLimits) {
|
|
438
|
+
if (!rawJson || typeof rawJson !== "object") {
|
|
439
|
+
throw new Error("AI output must be a JSON object");
|
|
440
|
+
}
|
|
441
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
442
|
+
const normalized = normalizeNodeTree(rawJson, usedIds);
|
|
443
|
+
normalized.id = originalNode.id;
|
|
444
|
+
normalized.type = originalNode.type;
|
|
445
|
+
const parsed = import_schema.NodeSchema.safeParse(normalized);
|
|
446
|
+
if (!parsed.success) {
|
|
447
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
448
|
+
throw new Error(`Refactored node schema validation failed: ${issues}`);
|
|
449
|
+
}
|
|
450
|
+
const secCheck = (0, import_core.validateDocumentSecurity)({ document: normalized }, securityLimits);
|
|
451
|
+
if (!secCheck.safe) {
|
|
452
|
+
const issues = secCheck.errors.map((e) => `${e.code}: ${e.message}`).join("; ");
|
|
453
|
+
throw new Error(`Refactored node security check failed: ${issues}`);
|
|
454
|
+
}
|
|
455
|
+
return parsed.data;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/server/engine.ts
|
|
459
|
+
var KubuildAiEngine = class {
|
|
460
|
+
options;
|
|
461
|
+
catalog;
|
|
462
|
+
constructor(options) {
|
|
463
|
+
if (!options.adapter) {
|
|
464
|
+
throw new Error("KubuildAiEngine requires an adapter instance");
|
|
465
|
+
}
|
|
466
|
+
this.options = options;
|
|
467
|
+
this.catalog = compileComponentCatalog(options.registry);
|
|
468
|
+
}
|
|
469
|
+
get adapterName() {
|
|
470
|
+
return this.options.adapter.name;
|
|
471
|
+
}
|
|
472
|
+
refreshCatalog() {
|
|
473
|
+
this.catalog = compileComponentCatalog(this.options.registry);
|
|
474
|
+
}
|
|
475
|
+
log(level, message, meta) {
|
|
476
|
+
if (this.options.logger) {
|
|
477
|
+
this.options.logger(level, message, meta);
|
|
478
|
+
} else if (this.options.debug) {
|
|
479
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
480
|
+
console.log(`[KUBUILD-AI ${level.toUpperCase()}] [${ts}] ${message}`, meta !== void 0 ? meta : "");
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
async generatePage(request, context) {
|
|
484
|
+
let rawText = "";
|
|
485
|
+
try {
|
|
486
|
+
this.log("info", `Generating full page for prompt: "${request.prompt}"`);
|
|
487
|
+
const systemPrompt = buildSystemPrompt({
|
|
488
|
+
catalog: this.catalog,
|
|
489
|
+
mode: "full-page",
|
|
490
|
+
prefix: this.options.systemPromptPrefix,
|
|
491
|
+
stylePreference: request.stylePreference
|
|
492
|
+
});
|
|
493
|
+
let userPrompt = `User Request: ${request.prompt}`;
|
|
494
|
+
if (request.tone) userPrompt += `
|
|
495
|
+
Tone: ${request.tone}`;
|
|
496
|
+
if (request.locale) userPrompt += `
|
|
497
|
+
Language/Locale: ${request.locale}`;
|
|
498
|
+
const jsonSchema = buildJsonSchemaForMode("full-page");
|
|
499
|
+
const result = await this.options.adapter.generate({
|
|
500
|
+
systemPrompt,
|
|
501
|
+
userPrompt,
|
|
502
|
+
jsonSchema,
|
|
503
|
+
signal: context?.signal
|
|
504
|
+
});
|
|
505
|
+
this.log("debug", "Raw model response (generatePage)", result.text);
|
|
506
|
+
rawText = result.text;
|
|
507
|
+
const rawJson = extractJsonFromResponse(rawText);
|
|
508
|
+
const document = normalizeAndValidatePageDocument(
|
|
509
|
+
rawJson,
|
|
510
|
+
this.options.securityLimits
|
|
511
|
+
);
|
|
512
|
+
if (request.metadata) {
|
|
513
|
+
document.metadata = import_schema2.DocumentMetadataSchema.parse({
|
|
514
|
+
...document.metadata,
|
|
515
|
+
...request.metadata
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
this.log(
|
|
519
|
+
"info",
|
|
520
|
+
`Full page successfully generated: "${document.metadata?.title || "Untitled"}" (${document.document.children?.length || 0} sections)`
|
|
521
|
+
);
|
|
522
|
+
return {
|
|
523
|
+
success: true,
|
|
524
|
+
data: document,
|
|
525
|
+
usage: result.usage,
|
|
526
|
+
rawModelResponse: rawText
|
|
527
|
+
};
|
|
528
|
+
} catch (err) {
|
|
529
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
530
|
+
this.log("error", `Generation failed: ${message}`);
|
|
531
|
+
return {
|
|
532
|
+
success: false,
|
|
533
|
+
error: {
|
|
534
|
+
code: "GENERATION_ERROR",
|
|
535
|
+
message
|
|
536
|
+
},
|
|
537
|
+
rawModelResponse: rawText || void 0
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async generateSection(request, context) {
|
|
542
|
+
let rawText = "";
|
|
543
|
+
try {
|
|
544
|
+
const systemPrompt = buildSystemPrompt({
|
|
545
|
+
catalog: this.catalog,
|
|
546
|
+
mode: "section",
|
|
547
|
+
prefix: this.options.systemPromptPrefix,
|
|
548
|
+
stylePreference: request.stylePreference
|
|
549
|
+
});
|
|
550
|
+
let userPrompt = `Generate a single section node for: ${request.prompt}`;
|
|
551
|
+
if (request.targetSectionType) {
|
|
552
|
+
userPrompt += `
|
|
553
|
+
Target Section Purpose: ${request.targetSectionType}`;
|
|
554
|
+
}
|
|
555
|
+
if (request.parentContext) {
|
|
556
|
+
userPrompt += `
|
|
557
|
+
Surrounding Page Context: ${request.parentContext}`;
|
|
558
|
+
}
|
|
559
|
+
const jsonSchema = buildJsonSchemaForMode("section");
|
|
560
|
+
const result = await this.options.adapter.generate({
|
|
561
|
+
systemPrompt,
|
|
562
|
+
userPrompt,
|
|
563
|
+
jsonSchema,
|
|
564
|
+
signal: context?.signal
|
|
565
|
+
});
|
|
566
|
+
this.log("debug", "Raw model response (generateSection)", result.text);
|
|
567
|
+
rawText = result.text;
|
|
568
|
+
const rawJson = extractJsonFromResponse(rawText);
|
|
569
|
+
const sectionNode = normalizeAndValidateSectionNode(
|
|
570
|
+
rawJson,
|
|
571
|
+
this.options.securityLimits
|
|
572
|
+
);
|
|
573
|
+
return {
|
|
574
|
+
success: true,
|
|
575
|
+
data: sectionNode,
|
|
576
|
+
usage: result.usage,
|
|
577
|
+
rawModelResponse: rawText
|
|
578
|
+
};
|
|
579
|
+
} catch (err) {
|
|
580
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
581
|
+
return {
|
|
582
|
+
success: false,
|
|
583
|
+
error: {
|
|
584
|
+
code: "SECTION_GENERATION_ERROR",
|
|
585
|
+
message
|
|
586
|
+
},
|
|
587
|
+
rawModelResponse: rawText || void 0
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async refactorNode(request, context) {
|
|
592
|
+
let rawText = "";
|
|
593
|
+
try {
|
|
594
|
+
const systemPrompt = buildSystemPrompt({
|
|
595
|
+
catalog: this.catalog,
|
|
596
|
+
mode: "refactor",
|
|
597
|
+
prefix: this.options.systemPromptPrefix,
|
|
598
|
+
stylePreference: request.stylePreference
|
|
599
|
+
});
|
|
600
|
+
const userPrompt = `Instruction: ${request.instruction}
|
|
601
|
+
|
|
602
|
+
Current Node:
|
|
603
|
+
${JSON.stringify(request.node, null, 2)}`;
|
|
604
|
+
const jsonSchema = buildJsonSchemaForMode("refactor");
|
|
605
|
+
const result = await this.options.adapter.generate({
|
|
606
|
+
systemPrompt,
|
|
607
|
+
userPrompt,
|
|
608
|
+
jsonSchema,
|
|
609
|
+
signal: context?.signal
|
|
610
|
+
});
|
|
611
|
+
rawText = result.text;
|
|
612
|
+
const rawJson = extractJsonFromResponse(rawText);
|
|
613
|
+
const updatedNode = normalizeAndValidateRefactoredNode(
|
|
614
|
+
rawJson,
|
|
615
|
+
request.node,
|
|
616
|
+
this.options.securityLimits
|
|
617
|
+
);
|
|
618
|
+
return {
|
|
619
|
+
success: true,
|
|
620
|
+
data: updatedNode,
|
|
621
|
+
usage: result.usage,
|
|
622
|
+
rawModelResponse: rawText
|
|
623
|
+
};
|
|
624
|
+
} catch (err) {
|
|
625
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
626
|
+
return {
|
|
627
|
+
success: false,
|
|
628
|
+
error: {
|
|
629
|
+
code: "REFACTOR_ERROR",
|
|
630
|
+
message
|
|
631
|
+
},
|
|
632
|
+
rawModelResponse: rawText || void 0
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Progressive Section Streaming Generator.
|
|
638
|
+
* Emits structured SSE events: status -> metadata -> section (one by one) -> complete.
|
|
639
|
+
*/
|
|
640
|
+
async *streamPage(request, context) {
|
|
641
|
+
try {
|
|
642
|
+
this.log("info", `[SSE] Starting streamPage for prompt: "${request.prompt}"`);
|
|
643
|
+
yield {
|
|
644
|
+
type: "status",
|
|
645
|
+
message: "Analyzing requirements and planning page sections..."
|
|
646
|
+
};
|
|
647
|
+
const planSystemPrompt = `
|
|
648
|
+
You are a web architect for the KUBUILD page builder.
|
|
649
|
+
Given the user's prompt, plan the website structure. Output pure JSON (no markdown fences, no explanatory text):
|
|
650
|
+
{
|
|
651
|
+
"title": "<Page Title>",
|
|
652
|
+
"description": "<Page Description>",
|
|
653
|
+
"pageStyles": {
|
|
654
|
+
"base": {
|
|
655
|
+
"backgroundColor": "#ffffff",
|
|
656
|
+
"fontFamily": "Inter, system-ui, sans-serif",
|
|
657
|
+
"color": "#111827",
|
|
658
|
+
"minHeight": "100vh"
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
"sections": [
|
|
662
|
+
{
|
|
663
|
+
"type": "<hero|features|pricing|testimonials|cta|contact|footer>",
|
|
664
|
+
"title": "<Brief title>",
|
|
665
|
+
"prompt": "<Detailed prompt for generating this specific section with components, layout, and content>"
|
|
666
|
+
}
|
|
667
|
+
]
|
|
668
|
+
}
|
|
669
|
+
Plan 3 cohesive, essential sections (e.g., hero, features, and cta) that fulfill the request.
|
|
670
|
+
`;
|
|
671
|
+
let planUserPrompt = `User Request: ${request.prompt}`;
|
|
672
|
+
if (request.stylePreference) {
|
|
673
|
+
planUserPrompt += `
|
|
674
|
+
Style Preference: ${request.stylePreference}`;
|
|
675
|
+
}
|
|
676
|
+
if (request.tone) planUserPrompt += `
|
|
677
|
+
Tone: ${request.tone}`;
|
|
678
|
+
if (request.locale) planUserPrompt += `
|
|
679
|
+
Locale: ${request.locale}`;
|
|
680
|
+
this.log("info", "[SSE] Generating website layout plan...");
|
|
681
|
+
const planResult = await this.options.adapter.generate({
|
|
682
|
+
systemPrompt: planSystemPrompt,
|
|
683
|
+
userPrompt: planUserPrompt,
|
|
684
|
+
signal: context?.signal
|
|
685
|
+
});
|
|
686
|
+
this.log("debug", "[SSE] Raw plan response from model", planResult.text);
|
|
687
|
+
let plan;
|
|
688
|
+
try {
|
|
689
|
+
plan = extractJsonFromResponse(planResult.text);
|
|
690
|
+
this.log("debug", "[SSE] Parsed plan successfully", plan);
|
|
691
|
+
} catch (parseErr) {
|
|
692
|
+
this.log("warn", "[SSE] Failed to parse plan JSON, using fallback plan", parseErr);
|
|
693
|
+
plan = {
|
|
694
|
+
title: "AI Generated Page",
|
|
695
|
+
description: "Generated by KUBUILD AI",
|
|
696
|
+
sections: [
|
|
697
|
+
{ type: "hero", title: "Hero Banner", prompt: `Hero section for: ${request.prompt}` },
|
|
698
|
+
{ type: "features", title: "Key Features", prompt: `Features grid for: ${request.prompt}` },
|
|
699
|
+
{ type: "cta", title: "Call To Action", prompt: `Call to action section for: ${request.prompt}` }
|
|
700
|
+
]
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const sectionsToGenerate = Array.isArray(plan.sections) && plan.sections.length > 0 ? plan.sections : [
|
|
704
|
+
{ type: "hero", title: "Hero Section", prompt: `Hero banner for: ${request.prompt}` },
|
|
705
|
+
{ type: "features", title: "Features", prompt: `Features grid for: ${request.prompt}` },
|
|
706
|
+
{ type: "cta", title: "Call To Action", prompt: `CTA section for: ${request.prompt}` }
|
|
707
|
+
];
|
|
708
|
+
const metadata = import_schema2.DocumentMetadataSchema.parse({
|
|
709
|
+
title: plan.title || "AI Generated Page",
|
|
710
|
+
description: plan.description || "Generated by KUBUILD AI",
|
|
711
|
+
author: "KUBUILD AI",
|
|
712
|
+
tags: ["ai-generated", "progressive-stream"],
|
|
713
|
+
category: "landing",
|
|
714
|
+
version: "1.0.0",
|
|
715
|
+
...request.metadata || {}
|
|
716
|
+
});
|
|
717
|
+
const rootPageNode = {
|
|
718
|
+
id: "root-page",
|
|
719
|
+
type: "page",
|
|
720
|
+
styles: plan.pageStyles || {
|
|
721
|
+
base: {
|
|
722
|
+
backgroundColor: "#ffffff",
|
|
723
|
+
fontFamily: "Inter, system-ui, sans-serif",
|
|
724
|
+
color: "#111827",
|
|
725
|
+
minHeight: "100vh"
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
children: []
|
|
729
|
+
};
|
|
730
|
+
this.log("info", `[SSE] Emitting metadata: "${metadata.title}"`);
|
|
731
|
+
yield {
|
|
732
|
+
type: "metadata",
|
|
733
|
+
metadata,
|
|
734
|
+
rootPageNode
|
|
735
|
+
};
|
|
736
|
+
this.log(
|
|
737
|
+
"info",
|
|
738
|
+
`[SSE] Plan ready with ${sectionsToGenerate.length} sections: ${sectionsToGenerate.map((s) => s.title).join(", ")}`
|
|
739
|
+
);
|
|
740
|
+
yield {
|
|
741
|
+
type: "status",
|
|
742
|
+
message: `Plan ready with ${sectionsToGenerate.length} sections: ${sectionsToGenerate.map((s) => s.title).join(", ")}`
|
|
743
|
+
};
|
|
744
|
+
const completedSections = [];
|
|
745
|
+
const total = sectionsToGenerate.length;
|
|
746
|
+
for (let i = 0; i < total; i++) {
|
|
747
|
+
if (context?.signal?.aborted) {
|
|
748
|
+
this.log("warn", "[SSE] Streaming aborted by client signal");
|
|
749
|
+
throw new Error("Streaming aborted by client");
|
|
750
|
+
}
|
|
751
|
+
const plannedSec = sectionsToGenerate[i];
|
|
752
|
+
this.log(
|
|
753
|
+
"info",
|
|
754
|
+
`[SSE] Generating section ${i + 1}/${total} [${plannedSec.type}]: "${plannedSec.title}"`
|
|
755
|
+
);
|
|
756
|
+
yield {
|
|
757
|
+
type: "status",
|
|
758
|
+
message: `Generating section ${i + 1}/${total} (${plannedSec.title})...`
|
|
759
|
+
};
|
|
760
|
+
const secRes = await this.generateSection(
|
|
761
|
+
{
|
|
762
|
+
prompt: plannedSec.prompt,
|
|
763
|
+
stylePreference: request.stylePreference,
|
|
764
|
+
targetSectionType: plannedSec.type,
|
|
765
|
+
parentContext: `Website: ${plan.title}. Previous sections: ${sectionsToGenerate.slice(0, i).map((s) => s.title).join(", ")}`
|
|
766
|
+
},
|
|
767
|
+
{ signal: context?.signal }
|
|
768
|
+
);
|
|
769
|
+
if (secRes.success && secRes.data) {
|
|
770
|
+
this.log(
|
|
771
|
+
"info",
|
|
772
|
+
`[SSE] Section ${i + 1}/${total} generated successfully: [${secRes.data.id || secRes.data.type}]`
|
|
773
|
+
);
|
|
774
|
+
completedSections.push(secRes.data);
|
|
775
|
+
yield {
|
|
776
|
+
type: "section",
|
|
777
|
+
index: i,
|
|
778
|
+
total,
|
|
779
|
+
section: secRes.data
|
|
780
|
+
};
|
|
781
|
+
} else {
|
|
782
|
+
this.log("error", `[SSE] Section ${i + 1}/${total} failed`, secRes.error?.message);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (completedSections.length === 0) {
|
|
786
|
+
this.log("error", "[SSE] All sections failed to generate.");
|
|
787
|
+
yield {
|
|
788
|
+
type: "error",
|
|
789
|
+
error: {
|
|
790
|
+
code: "STREAM_GENERATION_FAILED",
|
|
791
|
+
message: "All sections failed to generate. Please check server logs or retry."
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const finalDocument = {
|
|
797
|
+
schema: "stora.page",
|
|
798
|
+
version: "1.0.0",
|
|
799
|
+
metadata,
|
|
800
|
+
document: {
|
|
801
|
+
...rootPageNode,
|
|
802
|
+
children: completedSections
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
this.log(
|
|
806
|
+
"info",
|
|
807
|
+
`[SSE] All ${completedSections.length} sections generated. Emitting complete event.`
|
|
808
|
+
);
|
|
809
|
+
yield {
|
|
810
|
+
type: "complete",
|
|
811
|
+
document: finalDocument
|
|
812
|
+
};
|
|
813
|
+
} catch (err) {
|
|
814
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
815
|
+
this.log("error", `[SSE] Streaming failed: ${message}`);
|
|
816
|
+
yield {
|
|
817
|
+
type: "error",
|
|
818
|
+
error: {
|
|
819
|
+
code: "STREAM_ERROR",
|
|
820
|
+
message
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Builds the system/user prompt pair shared by `chat()` and `chatStream()` — the exact
|
|
827
|
+
* same document/selection grounding is used regardless of whether the response is
|
|
828
|
+
* streamed token-by-token or returned as a single request/response (STORA-515).
|
|
829
|
+
*/
|
|
830
|
+
buildChatPrompt(request) {
|
|
831
|
+
let systemPrompt = `You are the KUBUILD AI Assistant \u2014 an expert web designer, developer, and page architecture consultant.
|
|
832
|
+
Your role is to assist users with creating, designing, refining, and understanding web pages built with the KUBUILD builder.
|
|
833
|
+
Answer questions directly, concisely, and helpfully using friendly professional tone and markdown formatting.
|
|
834
|
+
|
|
835
|
+
### Available KUBUILD Components:
|
|
836
|
+
${this.catalog.map((c) => `- **${c.type}** (${c.category}): ${c.label}${c.description ? ` \u2014 ${c.description}` : ""}`).join("\n")}
|
|
837
|
+
`;
|
|
838
|
+
if (this.options.systemPromptPrefix) {
|
|
839
|
+
systemPrompt = `${this.options.systemPromptPrefix}
|
|
840
|
+
|
|
841
|
+
${systemPrompt}`;
|
|
842
|
+
}
|
|
843
|
+
if (request.systemPrompt) {
|
|
844
|
+
systemPrompt += `
|
|
845
|
+
Additional Context:
|
|
846
|
+
${request.systemPrompt}`;
|
|
847
|
+
}
|
|
848
|
+
if (request.currentDocument) {
|
|
849
|
+
const doc = request.currentDocument;
|
|
850
|
+
const sections = doc.document?.children || [];
|
|
851
|
+
const sectionSummary = sections.map((s, idx) => `Section ${idx + 1}: id="${s.id}", type="${s.type}", title="${s.props?.title || "untitled"}", childrenCount=${s.children?.length || 0}`).join("\n");
|
|
852
|
+
systemPrompt += `
|
|
853
|
+
### Current Page in Builder Canvas:
|
|
854
|
+
- Title: ${doc.metadata?.title || "Untitled Page"}
|
|
855
|
+
- Description: ${doc.metadata?.description || "N/A"}
|
|
856
|
+
- Total Sections: ${sections.length}
|
|
857
|
+
- Sections Summary:
|
|
858
|
+
${sectionSummary || "(Canvas is currently empty)"}
|
|
859
|
+
`;
|
|
860
|
+
}
|
|
861
|
+
if (request.selectedNodeId) {
|
|
862
|
+
systemPrompt += `
|
|
863
|
+
Currently Selected Component Node ID: "${request.selectedNodeId}"`;
|
|
864
|
+
}
|
|
865
|
+
const lastUserMsg = [...request.messages].reverse().find((m) => m.role === "user");
|
|
866
|
+
const userPrompt = lastUserMsg?.content || "Hello";
|
|
867
|
+
return { systemPrompt, userPrompt };
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Conversational Assistant / Q&A.
|
|
871
|
+
* Can discuss web design, explain KUBUILD components, review current page,
|
|
872
|
+
* suggest improvements, or recommend copywriting.
|
|
873
|
+
*
|
|
874
|
+
* Single request/response — unchanged since before STORA-515. See `chatStream()` for
|
|
875
|
+
* the token-level streaming variant.
|
|
876
|
+
*/
|
|
877
|
+
async chat(request, context) {
|
|
878
|
+
let rawText = "";
|
|
879
|
+
try {
|
|
880
|
+
if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
|
|
881
|
+
return {
|
|
882
|
+
success: false,
|
|
883
|
+
error: {
|
|
884
|
+
code: "INVALID_CHAT_REQUEST",
|
|
885
|
+
message: '"messages" array is required and must not be empty'
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
this.log("info", `Processing chat turn with ${request.messages.length} messages`);
|
|
890
|
+
const { systemPrompt, userPrompt } = this.buildChatPrompt(request);
|
|
891
|
+
const result = await this.options.adapter.generate({
|
|
892
|
+
systemPrompt,
|
|
893
|
+
userPrompt,
|
|
894
|
+
messages: request.messages,
|
|
895
|
+
signal: context?.signal
|
|
896
|
+
});
|
|
897
|
+
rawText = result.text.trim();
|
|
898
|
+
const assistantMessage = {
|
|
899
|
+
role: "assistant",
|
|
900
|
+
content: rawText,
|
|
901
|
+
timestamp: Date.now()
|
|
902
|
+
};
|
|
903
|
+
return {
|
|
904
|
+
success: true,
|
|
905
|
+
data: {
|
|
906
|
+
message: assistantMessage
|
|
907
|
+
},
|
|
908
|
+
usage: result.usage,
|
|
909
|
+
rawModelResponse: rawText
|
|
910
|
+
};
|
|
911
|
+
} catch (err) {
|
|
912
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
913
|
+
this.log("error", `Chat failed: ${message}`);
|
|
914
|
+
return {
|
|
915
|
+
success: false,
|
|
916
|
+
error: {
|
|
917
|
+
code: "CHAT_ERROR",
|
|
918
|
+
message
|
|
919
|
+
},
|
|
920
|
+
rawModelResponse: rawText || void 0
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Token-level streaming variant of `chat()` (STORA-515). Emits `chat-chunk` events as
|
|
926
|
+
* partial text arrives, then a terminal `chat-complete` event with the fully assembled
|
|
927
|
+
* assistant message — the same `AsyncIterable<AiStreamEvent>` mechanism `streamPage`
|
|
928
|
+
* already uses, so it can be piped over SSE by `createAiHandler`/consumed by
|
|
929
|
+
* `KubuildAiClient` with no separate transport.
|
|
930
|
+
*
|
|
931
|
+
* When the configured adapter does not implement `generateStream` (e.g. the generic
|
|
932
|
+
* custom/HTTP adapter), this falls back to a single non-streaming `generate()` call and
|
|
933
|
+
* emits the entire response as one `chat-chunk`, so callers never see an error just
|
|
934
|
+
* because the underlying provider can't stream — only a less granular update cadence.
|
|
935
|
+
*/
|
|
936
|
+
async *chatStream(request, context) {
|
|
937
|
+
try {
|
|
938
|
+
if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
|
|
939
|
+
yield {
|
|
940
|
+
type: "error",
|
|
941
|
+
error: {
|
|
942
|
+
code: "INVALID_CHAT_REQUEST",
|
|
943
|
+
message: '"messages" array is required and must not be empty'
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
this.log(
|
|
949
|
+
"info",
|
|
950
|
+
`[SSE] Starting chatStream with ${request.messages.length} messages (adapter: ${this.options.adapter.name})`
|
|
951
|
+
);
|
|
952
|
+
const { systemPrompt, userPrompt } = this.buildChatPrompt(request);
|
|
953
|
+
let accumulated = "";
|
|
954
|
+
let usage;
|
|
955
|
+
if (typeof this.options.adapter.generateStream === "function") {
|
|
956
|
+
const generator = this.options.adapter.generateStream({
|
|
957
|
+
systemPrompt,
|
|
958
|
+
userPrompt,
|
|
959
|
+
messages: request.messages,
|
|
960
|
+
signal: context?.signal
|
|
961
|
+
});
|
|
962
|
+
while (true) {
|
|
963
|
+
const { value, done } = await generator.next();
|
|
964
|
+
if (done) {
|
|
965
|
+
usage = value?.usage;
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
accumulated += value;
|
|
969
|
+
yield { type: "chat-chunk", delta: value, content: accumulated };
|
|
970
|
+
}
|
|
971
|
+
} else {
|
|
972
|
+
this.log(
|
|
973
|
+
"debug",
|
|
974
|
+
`Adapter "${this.options.adapter.name}" has no generateStream; falling back to non-streaming chat`
|
|
975
|
+
);
|
|
976
|
+
const result = await this.options.adapter.generate({
|
|
977
|
+
systemPrompt,
|
|
978
|
+
userPrompt,
|
|
979
|
+
messages: request.messages,
|
|
980
|
+
signal: context?.signal
|
|
981
|
+
});
|
|
982
|
+
accumulated = result.text;
|
|
983
|
+
usage = result.usage;
|
|
984
|
+
yield { type: "chat-chunk", delta: accumulated, content: accumulated };
|
|
985
|
+
}
|
|
986
|
+
const assistantMessage = {
|
|
987
|
+
role: "assistant",
|
|
988
|
+
content: accumulated.trim(),
|
|
989
|
+
timestamp: Date.now()
|
|
990
|
+
};
|
|
991
|
+
this.log("info", "[SSE] Chat stream completed", usage);
|
|
992
|
+
yield { type: "chat-complete", message: assistantMessage };
|
|
993
|
+
} catch (err) {
|
|
994
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
995
|
+
this.log("error", `[SSE] Chat streaming failed: ${message}`);
|
|
996
|
+
yield {
|
|
997
|
+
type: "error",
|
|
998
|
+
error: {
|
|
999
|
+
code: "CHAT_STREAM_ERROR",
|
|
1000
|
+
message
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
// src/server/handler.ts
|
|
1008
|
+
async function processAiRequest(engine, body, signal) {
|
|
1009
|
+
if (!body || typeof body !== "object") {
|
|
1010
|
+
return {
|
|
1011
|
+
status: 400,
|
|
1012
|
+
response: {
|
|
1013
|
+
success: false,
|
|
1014
|
+
error: {
|
|
1015
|
+
code: "BAD_REQUEST",
|
|
1016
|
+
message: "Request body must be a valid JSON object"
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
const payload = body;
|
|
1022
|
+
const mode = payload.mode || "full-page";
|
|
1023
|
+
if (mode === "full-page") {
|
|
1024
|
+
if (!payload.prompt || typeof payload.prompt !== "string") {
|
|
1025
|
+
return {
|
|
1026
|
+
status: 400,
|
|
1027
|
+
response: {
|
|
1028
|
+
success: false,
|
|
1029
|
+
error: {
|
|
1030
|
+
code: "INVALID_PROMPT",
|
|
1031
|
+
message: '"prompt" is required for full-page generation'
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
const result = await engine.generatePage(
|
|
1037
|
+
{
|
|
1038
|
+
prompt: payload.prompt,
|
|
1039
|
+
stylePreference: payload.stylePreference,
|
|
1040
|
+
tone: payload.tone,
|
|
1041
|
+
locale: payload.locale,
|
|
1042
|
+
metadata: payload.metadata
|
|
1043
|
+
},
|
|
1044
|
+
{ signal }
|
|
1045
|
+
);
|
|
1046
|
+
return {
|
|
1047
|
+
status: result.success ? 200 : 500,
|
|
1048
|
+
response: result
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
if (mode === "section") {
|
|
1052
|
+
if (!payload.prompt || typeof payload.prompt !== "string") {
|
|
1053
|
+
return {
|
|
1054
|
+
status: 400,
|
|
1055
|
+
response: {
|
|
1056
|
+
success: false,
|
|
1057
|
+
error: {
|
|
1058
|
+
code: "INVALID_PROMPT",
|
|
1059
|
+
message: '"prompt" is required for section generation'
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
const result = await engine.generateSection(
|
|
1065
|
+
{
|
|
1066
|
+
prompt: payload.prompt,
|
|
1067
|
+
stylePreference: payload.stylePreference,
|
|
1068
|
+
targetSectionType: payload.targetSectionType,
|
|
1069
|
+
parentContext: payload.parentContext
|
|
1070
|
+
},
|
|
1071
|
+
{ signal }
|
|
1072
|
+
);
|
|
1073
|
+
return {
|
|
1074
|
+
status: result.success ? 200 : 500,
|
|
1075
|
+
response: result
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
if (mode === "refactor") {
|
|
1079
|
+
if (!payload.node || !payload.instruction) {
|
|
1080
|
+
return {
|
|
1081
|
+
status: 400,
|
|
1082
|
+
response: {
|
|
1083
|
+
success: false,
|
|
1084
|
+
error: {
|
|
1085
|
+
code: "INVALID_REFACTOR_PARAMS",
|
|
1086
|
+
message: '"node" and "instruction" are required for refactoring'
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
const result = await engine.refactorNode(
|
|
1092
|
+
{
|
|
1093
|
+
node: payload.node,
|
|
1094
|
+
instruction: payload.instruction,
|
|
1095
|
+
stylePreference: payload.stylePreference
|
|
1096
|
+
},
|
|
1097
|
+
{ signal }
|
|
1098
|
+
);
|
|
1099
|
+
return {
|
|
1100
|
+
status: result.success ? 200 : 500,
|
|
1101
|
+
response: result
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
if (mode === "chat") {
|
|
1105
|
+
if (!payload.messages || !Array.isArray(payload.messages) || payload.messages.length === 0) {
|
|
1106
|
+
return {
|
|
1107
|
+
status: 400,
|
|
1108
|
+
response: {
|
|
1109
|
+
success: false,
|
|
1110
|
+
error: {
|
|
1111
|
+
code: "INVALID_CHAT_PARAMS",
|
|
1112
|
+
message: '"messages" array is required and must not be empty for chat mode'
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
const result = await engine.chat(
|
|
1118
|
+
{
|
|
1119
|
+
messages: payload.messages,
|
|
1120
|
+
currentDocument: payload.currentDocument,
|
|
1121
|
+
selectedNodeId: payload.selectedNodeId
|
|
1122
|
+
},
|
|
1123
|
+
{ signal }
|
|
1124
|
+
);
|
|
1125
|
+
return {
|
|
1126
|
+
status: result.success ? 200 : 500,
|
|
1127
|
+
response: result
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
status: 400,
|
|
1132
|
+
response: {
|
|
1133
|
+
success: false,
|
|
1134
|
+
error: {
|
|
1135
|
+
code: "UNKNOWN_MODE",
|
|
1136
|
+
message: `Unsupported mode: ${String(mode)}. Supported modes: 'full-page', 'section', 'refactor', 'chat'`
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function createAiHandler(engine, options) {
|
|
1142
|
+
return async (request) => {
|
|
1143
|
+
try {
|
|
1144
|
+
if (options?.beforeRequest) {
|
|
1145
|
+
const hookResult = await options.beforeRequest(request);
|
|
1146
|
+
if (hookResult instanceof Response) {
|
|
1147
|
+
return hookResult;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
if (request.method !== "POST") {
|
|
1151
|
+
return new Response(
|
|
1152
|
+
JSON.stringify({
|
|
1153
|
+
success: false,
|
|
1154
|
+
error: {
|
|
1155
|
+
code: "METHOD_NOT_ALLOWED",
|
|
1156
|
+
message: "Only POST requests are accepted"
|
|
1157
|
+
}
|
|
1158
|
+
}),
|
|
1159
|
+
{
|
|
1160
|
+
status: 405,
|
|
1161
|
+
headers: {
|
|
1162
|
+
"Content-Type": "application/json",
|
|
1163
|
+
Allow: "POST"
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
const body = await request.json().catch(() => null);
|
|
1169
|
+
if (body && typeof body === "object" && body.stream === true) {
|
|
1170
|
+
const mode = body.mode || "full-page";
|
|
1171
|
+
if (mode === "chat") {
|
|
1172
|
+
if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) {
|
|
1173
|
+
return new Response(
|
|
1174
|
+
JSON.stringify({
|
|
1175
|
+
success: false,
|
|
1176
|
+
error: {
|
|
1177
|
+
code: "INVALID_CHAT_PARAMS",
|
|
1178
|
+
message: '"messages" array is required and must not be empty for chat mode'
|
|
1179
|
+
}
|
|
1180
|
+
}),
|
|
1181
|
+
{
|
|
1182
|
+
status: 400,
|
|
1183
|
+
headers: { "Content-Type": "application/json" }
|
|
1184
|
+
}
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
const encoder2 = new TextEncoder();
|
|
1188
|
+
const readable2 = new ReadableStream({
|
|
1189
|
+
async start(controller) {
|
|
1190
|
+
try {
|
|
1191
|
+
for await (const event of engine.chatStream(
|
|
1192
|
+
{
|
|
1193
|
+
messages: body.messages,
|
|
1194
|
+
currentDocument: body.currentDocument,
|
|
1195
|
+
selectedNodeId: body.selectedNodeId
|
|
1196
|
+
},
|
|
1197
|
+
{ signal: request.signal }
|
|
1198
|
+
)) {
|
|
1199
|
+
controller.enqueue(
|
|
1200
|
+
encoder2.encode(`event: ${event.type}
|
|
1201
|
+
data: ${JSON.stringify(event)}
|
|
1202
|
+
|
|
1203
|
+
`)
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1208
|
+
controller.enqueue(
|
|
1209
|
+
encoder2.encode(
|
|
1210
|
+
`event: error
|
|
1211
|
+
data: ${JSON.stringify({ type: "error", error: { code: "CHAT_STREAM_ERROR", message } })}
|
|
1212
|
+
|
|
1213
|
+
`
|
|
1214
|
+
)
|
|
1215
|
+
);
|
|
1216
|
+
} finally {
|
|
1217
|
+
controller.close();
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
return new Response(readable2, {
|
|
1222
|
+
status: 200,
|
|
1223
|
+
headers: {
|
|
1224
|
+
"Content-Type": "text/event-stream",
|
|
1225
|
+
"Cache-Control": "no-cache, no-transform",
|
|
1226
|
+
Connection: "keep-alive"
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
if (!body.prompt || typeof body.prompt !== "string") {
|
|
1231
|
+
return new Response(
|
|
1232
|
+
JSON.stringify({
|
|
1233
|
+
success: false,
|
|
1234
|
+
error: {
|
|
1235
|
+
code: "INVALID_PROMPT",
|
|
1236
|
+
message: '"prompt" is required for streaming generation'
|
|
1237
|
+
}
|
|
1238
|
+
}),
|
|
1239
|
+
{
|
|
1240
|
+
status: 400,
|
|
1241
|
+
headers: { "Content-Type": "application/json" }
|
|
1242
|
+
}
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
const encoder = new TextEncoder();
|
|
1246
|
+
const readable = new ReadableStream({
|
|
1247
|
+
async start(controller) {
|
|
1248
|
+
try {
|
|
1249
|
+
for await (const event of engine.streamPage(
|
|
1250
|
+
{
|
|
1251
|
+
prompt: body.prompt,
|
|
1252
|
+
stylePreference: body.stylePreference,
|
|
1253
|
+
tone: body.tone,
|
|
1254
|
+
locale: body.locale,
|
|
1255
|
+
metadata: body.metadata
|
|
1256
|
+
},
|
|
1257
|
+
{ signal: request.signal }
|
|
1258
|
+
)) {
|
|
1259
|
+
controller.enqueue(
|
|
1260
|
+
encoder.encode(`event: ${event.type}
|
|
1261
|
+
data: ${JSON.stringify(event)}
|
|
1262
|
+
|
|
1263
|
+
`)
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1268
|
+
controller.enqueue(
|
|
1269
|
+
encoder.encode(
|
|
1270
|
+
`event: error
|
|
1271
|
+
data: ${JSON.stringify({ type: "error", error: { code: "STREAM_ERROR", message } })}
|
|
1272
|
+
|
|
1273
|
+
`
|
|
1274
|
+
)
|
|
1275
|
+
);
|
|
1276
|
+
} finally {
|
|
1277
|
+
controller.close();
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
return new Response(readable, {
|
|
1282
|
+
status: 200,
|
|
1283
|
+
headers: {
|
|
1284
|
+
"Content-Type": "text/event-stream",
|
|
1285
|
+
"Cache-Control": "no-cache, no-transform",
|
|
1286
|
+
Connection: "keep-alive"
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
const { status, response } = await processAiRequest(
|
|
1291
|
+
engine,
|
|
1292
|
+
body,
|
|
1293
|
+
request.signal
|
|
1294
|
+
);
|
|
1295
|
+
return new Response(JSON.stringify(response), {
|
|
1296
|
+
status,
|
|
1297
|
+
headers: {
|
|
1298
|
+
"Content-Type": "application/json"
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1303
|
+
return new Response(
|
|
1304
|
+
JSON.stringify({
|
|
1305
|
+
success: false,
|
|
1306
|
+
error: {
|
|
1307
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
1308
|
+
message
|
|
1309
|
+
}
|
|
1310
|
+
}),
|
|
1311
|
+
{
|
|
1312
|
+
status: 500,
|
|
1313
|
+
headers: {
|
|
1314
|
+
"Content-Type": "application/json"
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/server/adapters/stream-utils.ts
|
|
1323
|
+
async function* readSseDataLines(body, signal) {
|
|
1324
|
+
const reader = body.getReader();
|
|
1325
|
+
const decoder = new TextDecoder();
|
|
1326
|
+
let buffer = "";
|
|
1327
|
+
try {
|
|
1328
|
+
while (true) {
|
|
1329
|
+
if (signal?.aborted) {
|
|
1330
|
+
throw new Error("Streaming aborted by client");
|
|
1331
|
+
}
|
|
1332
|
+
const { done, value } = await reader.read();
|
|
1333
|
+
if (done) break;
|
|
1334
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1335
|
+
const frames = buffer.split("\n\n");
|
|
1336
|
+
buffer = frames.pop() || "";
|
|
1337
|
+
for (const frame of frames) {
|
|
1338
|
+
for (const line of frame.split("\n")) {
|
|
1339
|
+
const trimmed = line.trim();
|
|
1340
|
+
if (trimmed.startsWith("data:")) {
|
|
1341
|
+
yield trimmed.slice(5).trim();
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
} finally {
|
|
1347
|
+
reader.releaseLock();
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/server/adapters/gemini.ts
|
|
1352
|
+
var GeminiAdapter = class {
|
|
1353
|
+
name = "gemini";
|
|
1354
|
+
apiKey;
|
|
1355
|
+
model;
|
|
1356
|
+
temperature;
|
|
1357
|
+
baseUrl;
|
|
1358
|
+
constructor(options) {
|
|
1359
|
+
if (!options.apiKey) {
|
|
1360
|
+
throw new Error("GeminiAdapter requires an apiKey");
|
|
1361
|
+
}
|
|
1362
|
+
this.apiKey = options.apiKey;
|
|
1363
|
+
this.model = options.model || "gemini-2.5-flash";
|
|
1364
|
+
this.temperature = options.temperature ?? 0.4;
|
|
1365
|
+
this.baseUrl = options.baseUrl || "https://generativelanguage.googleapis.com/v1beta";
|
|
1366
|
+
}
|
|
1367
|
+
buildContents(params) {
|
|
1368
|
+
const { userPrompt, messages: chatMessages } = params;
|
|
1369
|
+
return chatMessages && chatMessages.length > 0 ? chatMessages.map((m) => ({
|
|
1370
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
1371
|
+
parts: [{ text: m.content }]
|
|
1372
|
+
})) : [
|
|
1373
|
+
{
|
|
1374
|
+
role: "user",
|
|
1375
|
+
parts: [{ text: userPrompt }]
|
|
1376
|
+
}
|
|
1377
|
+
];
|
|
1378
|
+
}
|
|
1379
|
+
async generate(params) {
|
|
1380
|
+
const { systemPrompt, messages: chatMessages, signal } = params;
|
|
1381
|
+
const url = `${this.baseUrl}/models/${encodeURIComponent(this.model)}:generateContent?key=${encodeURIComponent(this.apiKey)}`;
|
|
1382
|
+
const contents = this.buildContents(params);
|
|
1383
|
+
const generationConfig = {
|
|
1384
|
+
temperature: this.temperature
|
|
1385
|
+
};
|
|
1386
|
+
if (params.jsonSchema || !chatMessages && (systemPrompt.includes("JSON") || systemPrompt.includes("json"))) {
|
|
1387
|
+
generationConfig.responseMimeType = "application/json";
|
|
1388
|
+
}
|
|
1389
|
+
const body = {
|
|
1390
|
+
system_instruction: {
|
|
1391
|
+
parts: [{ text: systemPrompt }]
|
|
1392
|
+
},
|
|
1393
|
+
contents,
|
|
1394
|
+
generationConfig
|
|
1395
|
+
};
|
|
1396
|
+
const res = await fetch(url, {
|
|
1397
|
+
method: "POST",
|
|
1398
|
+
headers: {
|
|
1399
|
+
"Content-Type": "application/json"
|
|
1400
|
+
},
|
|
1401
|
+
body: JSON.stringify(body),
|
|
1402
|
+
signal
|
|
1403
|
+
});
|
|
1404
|
+
if (!res.ok) {
|
|
1405
|
+
const errText = await res.text().catch(() => "");
|
|
1406
|
+
throw new Error(
|
|
1407
|
+
`Gemini API error [${res.status} ${res.statusText}]: ${errText || "Unknown error"}`
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
const data = await res.json();
|
|
1411
|
+
const candidateText = data.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
1412
|
+
if (!candidateText) {
|
|
1413
|
+
throw new Error("Gemini returned an empty or invalid candidate response");
|
|
1414
|
+
}
|
|
1415
|
+
return {
|
|
1416
|
+
text: candidateText,
|
|
1417
|
+
usage: data.usageMetadata ? {
|
|
1418
|
+
promptTokens: data.usageMetadata.promptTokenCount,
|
|
1419
|
+
completionTokens: data.usageMetadata.candidatesTokenCount
|
|
1420
|
+
} : void 0
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Token-level streaming via Gemini's `streamGenerateContent` SSE endpoint (STORA-515).
|
|
1425
|
+
*/
|
|
1426
|
+
async *generateStream(params) {
|
|
1427
|
+
const { systemPrompt, signal } = params;
|
|
1428
|
+
const url = `${this.baseUrl}/models/${encodeURIComponent(this.model)}:streamGenerateContent?alt=sse&key=${encodeURIComponent(this.apiKey)}`;
|
|
1429
|
+
const contents = this.buildContents(params);
|
|
1430
|
+
const generationConfig = {
|
|
1431
|
+
temperature: this.temperature
|
|
1432
|
+
};
|
|
1433
|
+
const body = {
|
|
1434
|
+
system_instruction: {
|
|
1435
|
+
parts: [{ text: systemPrompt }]
|
|
1436
|
+
},
|
|
1437
|
+
contents,
|
|
1438
|
+
generationConfig
|
|
1439
|
+
};
|
|
1440
|
+
const res = await fetch(url, {
|
|
1441
|
+
method: "POST",
|
|
1442
|
+
headers: {
|
|
1443
|
+
"Content-Type": "application/json"
|
|
1444
|
+
},
|
|
1445
|
+
body: JSON.stringify(body),
|
|
1446
|
+
signal
|
|
1447
|
+
});
|
|
1448
|
+
if (!res.ok) {
|
|
1449
|
+
const errText = await res.text().catch(() => "");
|
|
1450
|
+
throw new Error(
|
|
1451
|
+
`Gemini API error [${res.status} ${res.statusText}]: ${errText || "Unknown error"}`
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
if (!res.body) {
|
|
1455
|
+
throw new Error("Gemini streaming response has no readable body");
|
|
1456
|
+
}
|
|
1457
|
+
let text = "";
|
|
1458
|
+
let usage;
|
|
1459
|
+
for await (const raw of readSseDataLines(res.body, signal)) {
|
|
1460
|
+
let parsed;
|
|
1461
|
+
try {
|
|
1462
|
+
parsed = JSON.parse(raw);
|
|
1463
|
+
} catch {
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
const delta = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
1467
|
+
if (typeof delta === "string" && delta.length > 0) {
|
|
1468
|
+
text += delta;
|
|
1469
|
+
yield delta;
|
|
1470
|
+
}
|
|
1471
|
+
if (parsed.usageMetadata) {
|
|
1472
|
+
usage = {
|
|
1473
|
+
promptTokens: parsed.usageMetadata.promptTokenCount,
|
|
1474
|
+
completionTokens: parsed.usageMetadata.candidatesTokenCount
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return { text, usage };
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1482
|
+
// src/server/adapters/openai.ts
|
|
1483
|
+
var OpenAiAdapter = class {
|
|
1484
|
+
name = "openai";
|
|
1485
|
+
apiKey;
|
|
1486
|
+
model;
|
|
1487
|
+
temperature;
|
|
1488
|
+
baseUrl;
|
|
1489
|
+
customHeaders;
|
|
1490
|
+
constructor(options) {
|
|
1491
|
+
if (!options.apiKey) {
|
|
1492
|
+
throw new Error("OpenAiAdapter requires an apiKey");
|
|
1493
|
+
}
|
|
1494
|
+
this.apiKey = options.apiKey;
|
|
1495
|
+
this.model = options.model || "gpt-4o";
|
|
1496
|
+
this.temperature = options.temperature ?? 0.4;
|
|
1497
|
+
this.baseUrl = (options.baseUrl || "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
1498
|
+
this.customHeaders = options.headers;
|
|
1499
|
+
}
|
|
1500
|
+
buildMessages(params) {
|
|
1501
|
+
const { systemPrompt, userPrompt, messages: chatMessages } = params;
|
|
1502
|
+
return chatMessages && chatMessages.length > 0 ? [
|
|
1503
|
+
{ role: "system", content: systemPrompt },
|
|
1504
|
+
...chatMessages.map((m) => ({ role: m.role, content: m.content }))
|
|
1505
|
+
] : [
|
|
1506
|
+
{ role: "system", content: systemPrompt },
|
|
1507
|
+
{ role: "user", content: userPrompt }
|
|
1508
|
+
];
|
|
1509
|
+
}
|
|
1510
|
+
async generate(params) {
|
|
1511
|
+
const { systemPrompt, signal } = params;
|
|
1512
|
+
const url = `${this.baseUrl}/chat/completions`;
|
|
1513
|
+
const messages = this.buildMessages(params);
|
|
1514
|
+
const chatMessages = params.messages;
|
|
1515
|
+
const body = {
|
|
1516
|
+
model: this.model,
|
|
1517
|
+
temperature: this.temperature,
|
|
1518
|
+
messages
|
|
1519
|
+
};
|
|
1520
|
+
if (params.jsonSchema || !chatMessages && (systemPrompt.includes("JSON") || systemPrompt.includes("json"))) {
|
|
1521
|
+
body.response_format = { type: "json_object" };
|
|
1522
|
+
}
|
|
1523
|
+
const res = await fetch(url, {
|
|
1524
|
+
method: "POST",
|
|
1525
|
+
headers: {
|
|
1526
|
+
"Content-Type": "application/json",
|
|
1527
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1528
|
+
...this.customHeaders
|
|
1529
|
+
},
|
|
1530
|
+
body: JSON.stringify(body),
|
|
1531
|
+
signal
|
|
1532
|
+
});
|
|
1533
|
+
if (!res.ok) {
|
|
1534
|
+
const errText = await res.text().catch(() => "");
|
|
1535
|
+
throw new Error(
|
|
1536
|
+
`OpenAI API error [${res.status} ${res.statusText}]: ${errText || "Unknown error"}`
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
const data = await res.json();
|
|
1540
|
+
const content = data.choices?.[0]?.message?.content;
|
|
1541
|
+
if (!content) {
|
|
1542
|
+
throw new Error("OpenAI returned an empty message content");
|
|
1543
|
+
}
|
|
1544
|
+
return {
|
|
1545
|
+
text: content,
|
|
1546
|
+
usage: data.usage ? {
|
|
1547
|
+
promptTokens: data.usage.prompt_tokens,
|
|
1548
|
+
completionTokens: data.usage.completion_tokens
|
|
1549
|
+
} : void 0
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Token-level streaming via OpenAI's `stream: true` chat completions mode (STORA-515).
|
|
1554
|
+
* Yields each `delta.content` fragment as it arrives and returns the fully assembled
|
|
1555
|
+
* text + usage (when the final chunk includes it) once the stream ends.
|
|
1556
|
+
*/
|
|
1557
|
+
async *generateStream(params) {
|
|
1558
|
+
const { signal } = params;
|
|
1559
|
+
const url = `${this.baseUrl}/chat/completions`;
|
|
1560
|
+
const messages = this.buildMessages(params);
|
|
1561
|
+
const body = {
|
|
1562
|
+
model: this.model,
|
|
1563
|
+
temperature: this.temperature,
|
|
1564
|
+
messages,
|
|
1565
|
+
stream: true
|
|
1566
|
+
};
|
|
1567
|
+
const res = await fetch(url, {
|
|
1568
|
+
method: "POST",
|
|
1569
|
+
headers: {
|
|
1570
|
+
"Content-Type": "application/json",
|
|
1571
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1572
|
+
...this.customHeaders
|
|
1573
|
+
},
|
|
1574
|
+
body: JSON.stringify(body),
|
|
1575
|
+
signal
|
|
1576
|
+
});
|
|
1577
|
+
if (!res.ok) {
|
|
1578
|
+
const errText = await res.text().catch(() => "");
|
|
1579
|
+
throw new Error(
|
|
1580
|
+
`OpenAI API error [${res.status} ${res.statusText}]: ${errText || "Unknown error"}`
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
if (!res.body) {
|
|
1584
|
+
throw new Error("OpenAI streaming response has no readable body");
|
|
1585
|
+
}
|
|
1586
|
+
let text = "";
|
|
1587
|
+
let usage;
|
|
1588
|
+
for await (const raw of readSseDataLines(res.body, signal)) {
|
|
1589
|
+
if (raw === "[DONE]") break;
|
|
1590
|
+
let parsed;
|
|
1591
|
+
try {
|
|
1592
|
+
parsed = JSON.parse(raw);
|
|
1593
|
+
} catch {
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
const delta = parsed.choices?.[0]?.delta?.content;
|
|
1597
|
+
if (typeof delta === "string" && delta.length > 0) {
|
|
1598
|
+
text += delta;
|
|
1599
|
+
yield delta;
|
|
1600
|
+
}
|
|
1601
|
+
if (parsed.usage) {
|
|
1602
|
+
usage = {
|
|
1603
|
+
promptTokens: parsed.usage.prompt_tokens,
|
|
1604
|
+
completionTokens: parsed.usage.completion_tokens
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return { text, usage };
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
|
|
1612
|
+
// src/server/adapters/anthropic.ts
|
|
1613
|
+
var AnthropicAdapter = class {
|
|
1614
|
+
name = "anthropic";
|
|
1615
|
+
apiKey;
|
|
1616
|
+
model;
|
|
1617
|
+
maxTokens;
|
|
1618
|
+
temperature;
|
|
1619
|
+
baseUrl;
|
|
1620
|
+
anthropicVersion;
|
|
1621
|
+
customHeaders;
|
|
1622
|
+
constructor(options) {
|
|
1623
|
+
if (!options.apiKey) {
|
|
1624
|
+
throw new Error("AnthropicAdapter requires an apiKey");
|
|
1625
|
+
}
|
|
1626
|
+
this.apiKey = options.apiKey;
|
|
1627
|
+
this.model = options.model || "claude-3-5-sonnet-20241022";
|
|
1628
|
+
this.maxTokens = options.maxTokens ?? 4096;
|
|
1629
|
+
this.temperature = options.temperature ?? 0.4;
|
|
1630
|
+
this.baseUrl = (options.baseUrl || "https://api.anthropic.com/v1").replace(/\/+$/, "");
|
|
1631
|
+
this.anthropicVersion = options.anthropicVersion || "2023-06-01";
|
|
1632
|
+
this.customHeaders = options.headers;
|
|
1633
|
+
}
|
|
1634
|
+
buildMessages(params) {
|
|
1635
|
+
const { userPrompt, messages: chatMessages } = params;
|
|
1636
|
+
return chatMessages && chatMessages.length > 0 ? chatMessages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => ({ role: m.role, content: m.content })) : [
|
|
1637
|
+
{
|
|
1638
|
+
role: "user",
|
|
1639
|
+
content: userPrompt
|
|
1640
|
+
}
|
|
1641
|
+
];
|
|
1642
|
+
}
|
|
1643
|
+
async generate(params) {
|
|
1644
|
+
const { systemPrompt, signal } = params;
|
|
1645
|
+
const url = `${this.baseUrl}/messages`;
|
|
1646
|
+
const messages = this.buildMessages(params);
|
|
1647
|
+
const body = {
|
|
1648
|
+
model: this.model,
|
|
1649
|
+
max_tokens: this.maxTokens,
|
|
1650
|
+
temperature: this.temperature,
|
|
1651
|
+
system: systemPrompt,
|
|
1652
|
+
messages
|
|
1653
|
+
};
|
|
1654
|
+
const res = await fetch(url, {
|
|
1655
|
+
method: "POST",
|
|
1656
|
+
headers: {
|
|
1657
|
+
"Content-Type": "application/json",
|
|
1658
|
+
"x-api-key": this.apiKey,
|
|
1659
|
+
"anthropic-version": this.anthropicVersion,
|
|
1660
|
+
...this.customHeaders
|
|
1661
|
+
},
|
|
1662
|
+
body: JSON.stringify(body),
|
|
1663
|
+
signal
|
|
1664
|
+
});
|
|
1665
|
+
if (!res.ok) {
|
|
1666
|
+
const errText = await res.text().catch(() => "");
|
|
1667
|
+
throw new Error(
|
|
1668
|
+
`Anthropic API error [${res.status} ${res.statusText}]: ${errText || "Unknown error"}`
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
const data = await res.json();
|
|
1672
|
+
const textPart = data.content?.find((c) => c.type === "text")?.text;
|
|
1673
|
+
if (!textPart) {
|
|
1674
|
+
throw new Error("Anthropic returned an empty text content");
|
|
1675
|
+
}
|
|
1676
|
+
return {
|
|
1677
|
+
text: textPart,
|
|
1678
|
+
usage: data.usage ? {
|
|
1679
|
+
promptTokens: data.usage.input_tokens,
|
|
1680
|
+
completionTokens: data.usage.output_tokens
|
|
1681
|
+
} : void 0
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Token-level streaming via Anthropic's `stream: true` Messages API (STORA-515).
|
|
1686
|
+
* Parses the `content_block_delta` (`text_delta`) events for incremental text and the
|
|
1687
|
+
* `message_start`/`message_delta` events for usage accounting.
|
|
1688
|
+
*/
|
|
1689
|
+
async *generateStream(params) {
|
|
1690
|
+
const { systemPrompt, signal } = params;
|
|
1691
|
+
const url = `${this.baseUrl}/messages`;
|
|
1692
|
+
const messages = this.buildMessages(params);
|
|
1693
|
+
const body = {
|
|
1694
|
+
model: this.model,
|
|
1695
|
+
max_tokens: this.maxTokens,
|
|
1696
|
+
temperature: this.temperature,
|
|
1697
|
+
system: systemPrompt,
|
|
1698
|
+
messages,
|
|
1699
|
+
stream: true
|
|
1700
|
+
};
|
|
1701
|
+
const res = await fetch(url, {
|
|
1702
|
+
method: "POST",
|
|
1703
|
+
headers: {
|
|
1704
|
+
"Content-Type": "application/json",
|
|
1705
|
+
"x-api-key": this.apiKey,
|
|
1706
|
+
"anthropic-version": this.anthropicVersion,
|
|
1707
|
+
...this.customHeaders
|
|
1708
|
+
},
|
|
1709
|
+
body: JSON.stringify(body),
|
|
1710
|
+
signal
|
|
1711
|
+
});
|
|
1712
|
+
if (!res.ok) {
|
|
1713
|
+
const errText = await res.text().catch(() => "");
|
|
1714
|
+
throw new Error(
|
|
1715
|
+
`Anthropic API error [${res.status} ${res.statusText}]: ${errText || "Unknown error"}`
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
if (!res.body) {
|
|
1719
|
+
throw new Error("Anthropic streaming response has no readable body");
|
|
1720
|
+
}
|
|
1721
|
+
let text = "";
|
|
1722
|
+
let promptTokens;
|
|
1723
|
+
let completionTokens;
|
|
1724
|
+
for await (const raw of readSseDataLines(res.body, signal)) {
|
|
1725
|
+
if (!raw) continue;
|
|
1726
|
+
let evt;
|
|
1727
|
+
try {
|
|
1728
|
+
evt = JSON.parse(raw);
|
|
1729
|
+
} catch {
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
if (evt.type === "content_block_delta" && evt.delta?.type === "text_delta") {
|
|
1733
|
+
const delta = evt.delta.text;
|
|
1734
|
+
if (typeof delta === "string" && delta.length > 0) {
|
|
1735
|
+
text += delta;
|
|
1736
|
+
yield delta;
|
|
1737
|
+
}
|
|
1738
|
+
} else if (evt.type === "message_start" && evt.message?.usage?.input_tokens !== void 0) {
|
|
1739
|
+
promptTokens = evt.message.usage.input_tokens;
|
|
1740
|
+
} else if (evt.type === "message_delta" && evt.usage?.output_tokens !== void 0) {
|
|
1741
|
+
completionTokens = evt.usage.output_tokens;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
return {
|
|
1745
|
+
text,
|
|
1746
|
+
usage: promptTokens !== void 0 || completionTokens !== void 0 ? { promptTokens, completionTokens } : void 0
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
|
|
1751
|
+
// src/server/adapters/custom.ts
|
|
1752
|
+
var CustomHttpAdapter = class {
|
|
1753
|
+
name;
|
|
1754
|
+
url;
|
|
1755
|
+
headers;
|
|
1756
|
+
formatRequestBody;
|
|
1757
|
+
parseResponse;
|
|
1758
|
+
constructor(options) {
|
|
1759
|
+
if (!options.url) {
|
|
1760
|
+
throw new Error("CustomHttpAdapter requires a url");
|
|
1761
|
+
}
|
|
1762
|
+
this.url = options.url;
|
|
1763
|
+
this.name = options.name || "custom-http";
|
|
1764
|
+
this.headers = options.headers;
|
|
1765
|
+
this.formatRequestBody = options.formatRequestBody || ((params) => ({
|
|
1766
|
+
messages: params.messages && params.messages.length > 0 ? [
|
|
1767
|
+
{ role: "system", content: params.systemPrompt },
|
|
1768
|
+
...params.messages.map((m) => ({ role: m.role, content: m.content }))
|
|
1769
|
+
] : [
|
|
1770
|
+
{ role: "system", content: params.systemPrompt },
|
|
1771
|
+
{ role: "user", content: params.userPrompt }
|
|
1772
|
+
]
|
|
1773
|
+
}));
|
|
1774
|
+
this.parseResponse = options.parseResponse || ((json) => {
|
|
1775
|
+
if (!json || typeof json !== "object") {
|
|
1776
|
+
throw new Error("Response is not a valid JSON object");
|
|
1777
|
+
}
|
|
1778
|
+
const obj = json;
|
|
1779
|
+
if (typeof obj.response === "string") return obj.response;
|
|
1780
|
+
if (typeof obj.text === "string") return obj.text;
|
|
1781
|
+
const choices = obj.choices;
|
|
1782
|
+
if (Array.isArray(choices) && choices[0]?.message?.content) {
|
|
1783
|
+
return choices[0].message.content;
|
|
1784
|
+
}
|
|
1785
|
+
const message = obj.message;
|
|
1786
|
+
if (message && typeof message.content === "string") {
|
|
1787
|
+
return message.content;
|
|
1788
|
+
}
|
|
1789
|
+
throw new Error("Unable to extract generated text from custom HTTP response");
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
async generate(params) {
|
|
1793
|
+
const body = this.formatRequestBody(params);
|
|
1794
|
+
const res = await fetch(this.url, {
|
|
1795
|
+
method: "POST",
|
|
1796
|
+
headers: {
|
|
1797
|
+
"Content-Type": "application/json",
|
|
1798
|
+
...this.headers
|
|
1799
|
+
},
|
|
1800
|
+
body: JSON.stringify(body),
|
|
1801
|
+
signal: params.signal
|
|
1802
|
+
});
|
|
1803
|
+
if (!res.ok) {
|
|
1804
|
+
const errText = await res.text().catch(() => "");
|
|
1805
|
+
throw new Error(
|
|
1806
|
+
`Custom HTTP adapter error [${res.status} ${res.statusText}]: ${errText || "Unknown error"}`
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
const json = await res.json();
|
|
1810
|
+
const text = this.parseResponse(json);
|
|
1811
|
+
return { text };
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1815
|
+
0 && (module.exports = {
|
|
1816
|
+
AnthropicAdapter,
|
|
1817
|
+
CustomHttpAdapter,
|
|
1818
|
+
GeminiAdapter,
|
|
1819
|
+
KubuildAiEngine,
|
|
1820
|
+
OpenAiAdapter,
|
|
1821
|
+
createAiHandler,
|
|
1822
|
+
processAiRequest
|
|
1823
|
+
});
|
|
1824
|
+
//# sourceMappingURL=index.cjs.map
|