@karixi/payload-ai 0.1.1
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/README.md +163 -0
- package/dist/base-BPwZbeh1.mjs +146 -0
- package/dist/base-BPwZbeh1.mjs.map +1 -0
- package/dist/content-generator-fPX2DL3g.mjs +220 -0
- package/dist/content-generator-fPX2DL3g.mjs.map +1 -0
- package/dist/dependency-resolver-BgsUvfRJ.mjs +62 -0
- package/dist/dependency-resolver-BgsUvfRJ.mjs.map +1 -0
- package/dist/index.d.mts +148 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +566 -0
- package/dist/index.mjs.map +1 -0
- package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
- package/dist/schema-reader-ZkoI6Pwi.mjs +279 -0
- package/dist/schema-reader-ZkoI6Pwi.mjs.map +1 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# @karixi/payload-ai
|
|
2
|
+
|
|
3
|
+
AI-powered data population and admin features for Payload CMS 3.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @karixi/payload-ai
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// payload.config.ts
|
|
15
|
+
import { buildConfig } from 'payload'
|
|
16
|
+
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
|
17
|
+
import { mcpPlugin } from '@payloadcms/plugin-mcp'
|
|
18
|
+
import { aiPlugin, getAITools, getAIPrompts, getAIResources } from '@karixi/payload-ai'
|
|
19
|
+
|
|
20
|
+
export default buildConfig({
|
|
21
|
+
plugins: [
|
|
22
|
+
aiPlugin({
|
|
23
|
+
provider: 'anthropic',
|
|
24
|
+
apiKeyEnvVar: 'ANTHROPIC_API_KEY',
|
|
25
|
+
features: {
|
|
26
|
+
adminUI: true,
|
|
27
|
+
devTools: false,
|
|
28
|
+
},
|
|
29
|
+
rollbackOnError: true,
|
|
30
|
+
}),
|
|
31
|
+
mcpPlugin({
|
|
32
|
+
tools: (incomingTools) => [...incomingTools, ...getAITools()],
|
|
33
|
+
prompts: (incomingPrompts) => [...incomingPrompts, ...getAIPrompts()],
|
|
34
|
+
resources: (incomingResources) => [...incomingResources, ...getAIResources()],
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
db: mongooseAdapter({ url: process.env.DATABASE_URI! }),
|
|
38
|
+
collections: [/* your collections */],
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## MCP Tools
|
|
43
|
+
|
|
44
|
+
| Tool | Description |
|
|
45
|
+
|------|-------------|
|
|
46
|
+
| `populateCollection` | Generate N documents for any collection using AI |
|
|
47
|
+
| `bulkPopulate` | Populate the entire site in dependency order |
|
|
48
|
+
| `getCollectionSchema` | View the analyzed schema for a collection |
|
|
49
|
+
| `listCollections` | List all collections with their populatable status |
|
|
50
|
+
|
|
51
|
+
## Claude Code Integration
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
claude mcp add --transport http Payload http://localhost:3000/api/mcp \
|
|
55
|
+
--header "Authorization: Bearer YOUR_API_KEY"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then use natural language in Claude Code:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Generate 10 blog posts for the posts collection
|
|
62
|
+
Populate all collections with realistic sample data
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Features
|
|
66
|
+
|
|
67
|
+
### Admin UI (`features.adminUI: true`)
|
|
68
|
+
|
|
69
|
+
- **AI Fill** button on individual documents — generates field values with one click
|
|
70
|
+
- **Bulk Generate** panel — populate any collection with N documents from the admin panel
|
|
71
|
+
- **Smart defaults** — AI infers sensible values from field names and collection context
|
|
72
|
+
- **Image alt text** — automatically generates alt text for uploaded media
|
|
73
|
+
|
|
74
|
+
### Dev Tools (`features.devTools: true`)
|
|
75
|
+
|
|
76
|
+
Requires `@anthropic-ai/stagehand` (optional peer dependency).
|
|
77
|
+
|
|
78
|
+
- `screenshot` — capture screenshots of your frontend
|
|
79
|
+
- `visual_diff` — compare before/after screenshots to catch regressions
|
|
80
|
+
- `test_form` — fill and submit forms programmatically
|
|
81
|
+
- `edit_test_fix` — AI-driven edit → test → fix loop for frontend issues
|
|
82
|
+
|
|
83
|
+
## Plugin Configuration
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
aiPlugin({
|
|
87
|
+
provider: 'anthropic' | 'openai', // AI provider
|
|
88
|
+
apiKeyEnvVar: string, // env var name for the API key
|
|
89
|
+
features: {
|
|
90
|
+
adminUI: boolean, // default: false
|
|
91
|
+
devTools: boolean, // default: false
|
|
92
|
+
},
|
|
93
|
+
collections: {
|
|
94
|
+
posts: {
|
|
95
|
+
fields: {
|
|
96
|
+
title: { enabled: true, prompt: 'A compelling blog post title' },
|
|
97
|
+
status: { enabled: false },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
rateLimit: {
|
|
102
|
+
maxTokensPerBatch: 100000,
|
|
103
|
+
delayBetweenRequests: 200, // ms
|
|
104
|
+
maxConcurrentRequests: 3,
|
|
105
|
+
},
|
|
106
|
+
rollbackOnError: true, // delete created docs if bulk run fails
|
|
107
|
+
maxConcurrentBrowserInstances: 2, // for dev tools
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## API
|
|
112
|
+
|
|
113
|
+
### Plugin entry points
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { aiPlugin } from '@karixi/payload-ai'
|
|
117
|
+
import { getAITools } from '@karixi/payload-ai'
|
|
118
|
+
import { getAIPrompts } from '@karixi/payload-ai'
|
|
119
|
+
import { getAIResources } from '@karixi/payload-ai'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Types
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import type {
|
|
126
|
+
AIPluginConfig,
|
|
127
|
+
AIProvider,
|
|
128
|
+
CollectionAIConfig,
|
|
129
|
+
CollectionSchema,
|
|
130
|
+
DeletionLogEntry,
|
|
131
|
+
FieldSchema,
|
|
132
|
+
ProgressEvent,
|
|
133
|
+
RelationshipInfo,
|
|
134
|
+
} from '@karixi/payload-ai'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Core utilities (advanced)
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// Analyze collection fields into a structured schema
|
|
141
|
+
import { analyzeFields } from '@karixi/payload-ai/core/field-analyzer'
|
|
142
|
+
|
|
143
|
+
// Resolve creation order for collections with relationships
|
|
144
|
+
import { resolveCreationOrder, getDependencies } from '@karixi/payload-ai/core/dependency-resolver'
|
|
145
|
+
|
|
146
|
+
// Convert plain text or structured content to Lexical rich text format
|
|
147
|
+
import { textToLexical, contentToLexical } from '@karixi/payload-ai/generate/richtext-generator'
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Requirements
|
|
151
|
+
|
|
152
|
+
- `payload` ^3.79.0
|
|
153
|
+
- `@payloadcms/plugin-mcp` ^3.79.0
|
|
154
|
+
- Node.js 20+ or Bun 1.1+
|
|
155
|
+
- Optional: `@anthropic-ai/stagehand` (for dev tools feature)
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
bun run build # compile TypeScript
|
|
161
|
+
bun run typecheck # type-check without emitting
|
|
162
|
+
bun run test # run unit tests
|
|
163
|
+
```
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
+
//#region src/core/providers/anthropic.ts
|
|
3
|
+
function isAnthropicResponse(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && "content" in value && Array.isArray(value.content);
|
|
5
|
+
}
|
|
6
|
+
function createAnthropicProvider(apiKey) {
|
|
7
|
+
async function callAPI(messages) {
|
|
8
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: {
|
|
11
|
+
"x-api-key": apiKey,
|
|
12
|
+
"anthropic-version": "2023-06-01",
|
|
13
|
+
"content-type": "application/json"
|
|
14
|
+
},
|
|
15
|
+
body: JSON.stringify({
|
|
16
|
+
model: "claude-sonnet-4-20250514",
|
|
17
|
+
max_tokens: 8192,
|
|
18
|
+
messages
|
|
19
|
+
})
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
const errorText = await response.text();
|
|
23
|
+
throw new Error(`Anthropic API error ${response.status}: ${errorText}`);
|
|
24
|
+
}
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
if (!isAnthropicResponse(data)) throw new Error("Unexpected Anthropic API response shape");
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
async generate(prompt, _outputSchema) {
|
|
31
|
+
const textBlock = (await callAPI([{
|
|
32
|
+
role: "user",
|
|
33
|
+
content: `${prompt}\n\nRespond with ONLY a valid JSON array. No markdown, no explanation, just the JSON array.`
|
|
34
|
+
}])).content.find((block) => block.type === "text");
|
|
35
|
+
if (!textBlock || !("text" in textBlock) || typeof textBlock.text !== "string") throw new Error("No text content in Anthropic response");
|
|
36
|
+
const jsonText = textBlock.text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
|
|
37
|
+
const parsed = JSON.parse(jsonText);
|
|
38
|
+
if (!Array.isArray(parsed)) throw new Error("Anthropic response is not a JSON array");
|
|
39
|
+
return parsed;
|
|
40
|
+
},
|
|
41
|
+
async analyzeImage(imageBuffer) {
|
|
42
|
+
const base64 = imageBuffer.toString("base64");
|
|
43
|
+
let mediaType = "image/jpeg";
|
|
44
|
+
if (imageBuffer[0] === 137 && imageBuffer[1] === 80) mediaType = "image/png";
|
|
45
|
+
else if (imageBuffer[0] === 71 && imageBuffer[1] === 73) mediaType = "image/gif";
|
|
46
|
+
else if (imageBuffer[0] === 82 && imageBuffer[1] === 73) mediaType = "image/webp";
|
|
47
|
+
const textBlock = (await callAPI([{
|
|
48
|
+
role: "user",
|
|
49
|
+
content: [{
|
|
50
|
+
type: "image",
|
|
51
|
+
source: {
|
|
52
|
+
type: "base64",
|
|
53
|
+
media_type: mediaType,
|
|
54
|
+
data: base64
|
|
55
|
+
}
|
|
56
|
+
}, {
|
|
57
|
+
type: "text",
|
|
58
|
+
text: "Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation."
|
|
59
|
+
}]
|
|
60
|
+
}])).content.find((block) => block.type === "text");
|
|
61
|
+
if (!textBlock || !("text" in textBlock) || typeof textBlock.text !== "string") throw new Error("No text content in Anthropic image analysis response");
|
|
62
|
+
return textBlock.text.trim();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/core/providers/openai.ts
|
|
68
|
+
function isOpenAIResponse(value) {
|
|
69
|
+
return typeof value === "object" && value !== null && "choices" in value && Array.isArray(value.choices);
|
|
70
|
+
}
|
|
71
|
+
function createOpenAIProvider(apiKey) {
|
|
72
|
+
async function callAPI(messages, jsonMode) {
|
|
73
|
+
const body = {
|
|
74
|
+
model: "gpt-4o",
|
|
75
|
+
messages
|
|
76
|
+
};
|
|
77
|
+
if (jsonMode) body.response_format = { type: "json_object" };
|
|
78
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: {
|
|
81
|
+
Authorization: `Bearer ${apiKey}`,
|
|
82
|
+
"content-type": "application/json"
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify(body)
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
const errorText = await response.text();
|
|
88
|
+
throw new Error(`OpenAI API error ${response.status}: ${errorText}`);
|
|
89
|
+
}
|
|
90
|
+
const data = await response.json();
|
|
91
|
+
if (!isOpenAIResponse(data)) throw new Error("Unexpected OpenAI API response shape");
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
async generate(prompt, _outputSchema) {
|
|
96
|
+
const choice = (await callAPI([{
|
|
97
|
+
role: "system",
|
|
98
|
+
content: "You are a data generation assistant. Always respond with valid JSON only. When asked for an array, wrap it in {\"items\": [...]} so json_object mode is satisfied."
|
|
99
|
+
}, {
|
|
100
|
+
role: "user",
|
|
101
|
+
content: `${prompt}\n\nRespond with JSON object {"items": [...]} where items is the array of generated documents.`
|
|
102
|
+
}], true)).choices[0];
|
|
103
|
+
if (!choice || choice.message.content === null) throw new Error("No content in OpenAI response");
|
|
104
|
+
const parsed = JSON.parse(choice.message.content);
|
|
105
|
+
if (typeof parsed === "object" && parsed !== null && "items" in parsed && Array.isArray(parsed.items)) return parsed.items;
|
|
106
|
+
if (Array.isArray(parsed)) return parsed;
|
|
107
|
+
throw new Error("OpenAI response is not a JSON array");
|
|
108
|
+
},
|
|
109
|
+
async analyzeImage(imageBuffer) {
|
|
110
|
+
const base64 = imageBuffer.toString("base64");
|
|
111
|
+
let mediaType = "image/jpeg";
|
|
112
|
+
if (imageBuffer[0] === 137 && imageBuffer[1] === 80) mediaType = "image/png";
|
|
113
|
+
else if (imageBuffer[0] === 71 && imageBuffer[1] === 73) mediaType = "image/gif";
|
|
114
|
+
else if (imageBuffer[0] === 82 && imageBuffer[1] === 73) mediaType = "image/webp";
|
|
115
|
+
const choice = (await callAPI([{
|
|
116
|
+
role: "user",
|
|
117
|
+
content: [{
|
|
118
|
+
type: "image_url",
|
|
119
|
+
image_url: { url: `data:${mediaType};base64,${base64}` }
|
|
120
|
+
}, {
|
|
121
|
+
type: "text",
|
|
122
|
+
text: "Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation."
|
|
123
|
+
}]
|
|
124
|
+
}], false)).choices[0];
|
|
125
|
+
if (!choice || choice.message.content === null) throw new Error("No content in OpenAI image analysis response");
|
|
126
|
+
return choice.message.content.trim();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/core/providers/base.ts
|
|
132
|
+
var base_exports = /* @__PURE__ */ __exportAll({ createProvider: () => createProvider });
|
|
133
|
+
function createProvider(config) {
|
|
134
|
+
switch (config.provider) {
|
|
135
|
+
case "anthropic": return createAnthropicProvider(config.apiKey);
|
|
136
|
+
case "openai": return createOpenAIProvider(config.apiKey);
|
|
137
|
+
default: {
|
|
138
|
+
const _exhaustive = config.provider;
|
|
139
|
+
throw new Error(`Unknown provider: ${String(_exhaustive)}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
export { createProvider as n, base_exports as t };
|
|
145
|
+
|
|
146
|
+
//# sourceMappingURL=base-BPwZbeh1.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-BPwZbeh1.mjs","names":[],"sources":["../src/core/providers/anthropic.ts","../src/core/providers/openai.ts","../src/core/providers/base.ts"],"sourcesContent":["import type { AIProvider } from '../../types.js'\n\ntype AnthropicMessage = {\n role: 'user' | 'assistant'\n content: string | AnthropicContentBlock[]\n}\n\ntype AnthropicContentBlock =\n | { type: 'text'; text: string }\n | { type: 'image'; source: { type: 'base64'; media_type: string; data: string } }\n\ntype AnthropicResponse = {\n content: Array<{ type: string; text?: string }>\n usage?: { input_tokens: number; output_tokens: number }\n}\n\nfunction isAnthropicResponse(value: unknown): value is AnthropicResponse {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'content' in value &&\n Array.isArray((value as AnthropicResponse).content)\n )\n}\n\nexport function createAnthropicProvider(apiKey: string): AIProvider {\n async function callAPI(messages: AnthropicMessage[]): Promise<AnthropicResponse> {\n const response = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: {\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n model: 'claude-sonnet-4-20250514',\n max_tokens: 8192,\n messages,\n }),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(`Anthropic API error ${response.status}: ${errorText}`)\n }\n\n const data: unknown = await response.json()\n if (!isAnthropicResponse(data)) {\n throw new Error('Unexpected Anthropic API response shape')\n }\n return data\n }\n\n return {\n async generate(prompt: string, _outputSchema: Record<string, unknown>): Promise<unknown[]> {\n const messages: AnthropicMessage[] = [\n {\n role: 'user',\n content: `${prompt}\\n\\nRespond with ONLY a valid JSON array. No markdown, no explanation, just the JSON array.`,\n },\n ]\n\n const data = await callAPI(messages)\n const textBlock = data.content.find((block) => block.type === 'text')\n if (!textBlock || !('text' in textBlock) || typeof textBlock.text !== 'string') {\n throw new Error('No text content in Anthropic response')\n }\n\n const text = textBlock.text.trim()\n // Strip markdown code fences if present\n const jsonText = text\n .replace(/^```(?:json)?\\s*/i, '')\n .replace(/\\s*```\\s*$/, '')\n .trim()\n\n const parsed: unknown = JSON.parse(jsonText)\n if (!Array.isArray(parsed)) {\n throw new Error('Anthropic response is not a JSON array')\n }\n return parsed\n },\n\n async analyzeImage(imageBuffer: Buffer): Promise<string> {\n const base64 = imageBuffer.toString('base64')\n // Detect image type from buffer magic bytes\n let mediaType = 'image/jpeg'\n if (imageBuffer[0] === 0x89 && imageBuffer[1] === 0x50) mediaType = 'image/png'\n else if (imageBuffer[0] === 0x47 && imageBuffer[1] === 0x49) mediaType = 'image/gif'\n else if (imageBuffer[0] === 0x52 && imageBuffer[1] === 0x49) mediaType = 'image/webp'\n\n const messages: AnthropicMessage[] = [\n {\n role: 'user',\n content: [\n {\n type: 'image',\n source: { type: 'base64', media_type: mediaType, data: base64 },\n },\n {\n type: 'text',\n text: 'Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation.',\n },\n ],\n },\n ]\n\n const data = await callAPI(messages)\n const textBlock = data.content.find((block) => block.type === 'text')\n if (!textBlock || !('text' in textBlock) || typeof textBlock.text !== 'string') {\n throw new Error('No text content in Anthropic image analysis response')\n }\n return textBlock.text.trim()\n },\n }\n}\n","import type { AIProvider } from '../../types.js'\n\ntype OpenAIMessage = {\n role: 'user' | 'assistant' | 'system'\n content: string | OpenAIContentBlock[]\n}\n\ntype OpenAIContentBlock =\n | { type: 'text'; text: string }\n | { type: 'image_url'; image_url: { url: string } }\n\ntype OpenAIResponse = {\n choices: Array<{\n message: { role: string; content: string | null }\n }>\n usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number }\n}\n\nfunction isOpenAIResponse(value: unknown): value is OpenAIResponse {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'choices' in value &&\n Array.isArray((value as OpenAIResponse).choices)\n )\n}\n\nexport function createOpenAIProvider(apiKey: string): AIProvider {\n async function callAPI(messages: OpenAIMessage[], jsonMode: boolean): Promise<OpenAIResponse> {\n const body: Record<string, unknown> = {\n model: 'gpt-4o',\n messages,\n }\n if (jsonMode) {\n body.response_format = { type: 'json_object' }\n }\n\n const response = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(`OpenAI API error ${response.status}: ${errorText}`)\n }\n\n const data: unknown = await response.json()\n if (!isOpenAIResponse(data)) {\n throw new Error('Unexpected OpenAI API response shape')\n }\n return data\n }\n\n return {\n async generate(prompt: string, _outputSchema: Record<string, unknown>): Promise<unknown[]> {\n const messages: OpenAIMessage[] = [\n {\n role: 'system',\n content:\n 'You are a data generation assistant. Always respond with valid JSON only. When asked for an array, wrap it in {\"items\": [...]} so json_object mode is satisfied.',\n },\n {\n role: 'user',\n content: `${prompt}\\n\\nRespond with JSON object {\"items\": [...]} where items is the array of generated documents.`,\n },\n ]\n\n const data = await callAPI(messages, true)\n const choice = data.choices[0]\n if (!choice || choice.message.content === null) {\n throw new Error('No content in OpenAI response')\n }\n\n const parsed: unknown = JSON.parse(choice.message.content)\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n 'items' in parsed &&\n Array.isArray((parsed as { items: unknown }).items)\n ) {\n return (parsed as { items: unknown[] }).items\n }\n if (Array.isArray(parsed)) {\n return parsed\n }\n throw new Error('OpenAI response is not a JSON array')\n },\n\n async analyzeImage(imageBuffer: Buffer): Promise<string> {\n const base64 = imageBuffer.toString('base64')\n // Detect image type from buffer magic bytes\n let mediaType = 'image/jpeg'\n if (imageBuffer[0] === 0x89 && imageBuffer[1] === 0x50) mediaType = 'image/png'\n else if (imageBuffer[0] === 0x47 && imageBuffer[1] === 0x49) mediaType = 'image/gif'\n else if (imageBuffer[0] === 0x52 && imageBuffer[1] === 0x49) mediaType = 'image/webp'\n\n const dataUrl = `data:${mediaType};base64,${base64}`\n\n const messages: OpenAIMessage[] = [\n {\n role: 'user',\n content: [\n {\n type: 'image_url',\n image_url: { url: dataUrl },\n },\n {\n type: 'text',\n text: 'Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation.',\n },\n ],\n },\n ]\n\n const data = await callAPI(messages, false)\n const choice = data.choices[0]\n if (!choice || choice.message.content === null) {\n throw new Error('No content in OpenAI image analysis response')\n }\n return choice.message.content.trim()\n },\n }\n}\n","import type { AIProvider } from '../../types.js'\nimport { createAnthropicProvider } from './anthropic.js'\nimport { createOpenAIProvider } from './openai.js'\n\nexport type { AIProvider }\n\nexport type ProviderConfig = {\n provider: 'anthropic' | 'openai'\n apiKey: string\n}\n\nexport function createProvider(config: ProviderConfig): AIProvider {\n switch (config.provider) {\n case 'anthropic':\n return createAnthropicProvider(config.apiKey)\n case 'openai':\n return createOpenAIProvider(config.apiKey)\n default: {\n const _exhaustive: never = config.provider\n throw new Error(`Unknown provider: ${String(_exhaustive)}`)\n }\n }\n}\n"],"mappings":";;AAgBA,SAAS,oBAAoB,OAA4C;AACvE,QACE,OAAO,UAAU,YACjB,UAAU,QACV,aAAa,SACb,MAAM,QAAS,MAA4B,QAAQ;;AAIvD,SAAgB,wBAAwB,QAA4B;CAClE,eAAe,QAAQ,UAA0D;EAC/E,MAAM,WAAW,MAAM,MAAM,yCAAyC;GACpE,QAAQ;GACR,SAAS;IACP,aAAa;IACb,qBAAqB;IACrB,gBAAgB;IACjB;GACD,MAAM,KAAK,UAAU;IACnB,OAAO;IACP,YAAY;IACZ;IACD,CAAC;GACH,CAAC;AAEF,MAAI,CAAC,SAAS,IAAI;GAChB,MAAM,YAAY,MAAM,SAAS,MAAM;AACvC,SAAM,IAAI,MAAM,uBAAuB,SAAS,OAAO,IAAI,YAAY;;EAGzE,MAAM,OAAgB,MAAM,SAAS,MAAM;AAC3C,MAAI,CAAC,oBAAoB,KAAK,CAC5B,OAAM,IAAI,MAAM,0CAA0C;AAE5D,SAAO;;AAGT,QAAO;EACL,MAAM,SAAS,QAAgB,eAA4D;GASzF,MAAM,aADO,MAAM,QAPkB,CACnC;IACE,MAAM;IACN,SAAS,GAAG,OAAO;IACpB,CACF,CAEmC,EACb,QAAQ,MAAM,UAAU,MAAM,SAAS,OAAO;AACrE,OAAI,CAAC,aAAa,EAAE,UAAU,cAAc,OAAO,UAAU,SAAS,SACpE,OAAM,IAAI,MAAM,wCAAwC;GAK1D,MAAM,WAFO,UAAU,KAAK,MAAM,CAG/B,QAAQ,qBAAqB,GAAG,CAChC,QAAQ,cAAc,GAAG,CACzB,MAAM;GAET,MAAM,SAAkB,KAAK,MAAM,SAAS;AAC5C,OAAI,CAAC,MAAM,QAAQ,OAAO,CACxB,OAAM,IAAI,MAAM,yCAAyC;AAE3D,UAAO;;EAGT,MAAM,aAAa,aAAsC;GACvD,MAAM,SAAS,YAAY,SAAS,SAAS;GAE7C,IAAI,YAAY;AAChB,OAAI,YAAY,OAAO,OAAQ,YAAY,OAAO,GAAM,aAAY;YAC3D,YAAY,OAAO,MAAQ,YAAY,OAAO,GAAM,aAAY;YAChE,YAAY,OAAO,MAAQ,YAAY,OAAO,GAAM,aAAY;GAmBzE,MAAM,aADO,MAAM,QAhBkB,CACnC;IACE,MAAM;IACN,SAAS,CACP;KACE,MAAM;KACN,QAAQ;MAAE,MAAM;MAAU,YAAY;MAAW,MAAM;MAAQ;KAChE,EACD;KACE,MAAM;KACN,MAAM;KACP,CACF;IACF,CACF,CAEmC,EACb,QAAQ,MAAM,UAAU,MAAM,SAAS,OAAO;AACrE,OAAI,CAAC,aAAa,EAAE,UAAU,cAAc,OAAO,UAAU,SAAS,SACpE,OAAM,IAAI,MAAM,uDAAuD;AAEzE,UAAO,UAAU,KAAK,MAAM;;EAE/B;;;;AC/FH,SAAS,iBAAiB,OAAyC;AACjE,QACE,OAAO,UAAU,YACjB,UAAU,QACV,aAAa,SACb,MAAM,QAAS,MAAyB,QAAQ;;AAIpD,SAAgB,qBAAqB,QAA4B;CAC/D,eAAe,QAAQ,UAA2B,UAA4C;EAC5F,MAAM,OAAgC;GACpC,OAAO;GACP;GACD;AACD,MAAI,SACF,MAAK,kBAAkB,EAAE,MAAM,eAAe;EAGhD,MAAM,WAAW,MAAM,MAAM,8CAA8C;GACzE,QAAQ;GACR,SAAS;IACP,eAAe,UAAU;IACzB,gBAAgB;IACjB;GACD,MAAM,KAAK,UAAU,KAAK;GAC3B,CAAC;AAEF,MAAI,CAAC,SAAS,IAAI;GAChB,MAAM,YAAY,MAAM,SAAS,MAAM;AACvC,SAAM,IAAI,MAAM,oBAAoB,SAAS,OAAO,IAAI,YAAY;;EAGtE,MAAM,OAAgB,MAAM,SAAS,MAAM;AAC3C,MAAI,CAAC,iBAAiB,KAAK,CACzB,OAAM,IAAI,MAAM,uCAAuC;AAEzD,SAAO;;AAGT,QAAO;EACL,MAAM,SAAS,QAAgB,eAA4D;GAczF,MAAM,UADO,MAAM,QAZe,CAChC;IACE,MAAM;IACN,SACE;IACH,EACD;IACE,MAAM;IACN,SAAS,GAAG,OAAO;IACpB,CACF,EAEoC,KAAK,EACtB,QAAQ;AAC5B,OAAI,CAAC,UAAU,OAAO,QAAQ,YAAY,KACxC,OAAM,IAAI,MAAM,gCAAgC;GAGlD,MAAM,SAAkB,KAAK,MAAM,OAAO,QAAQ,QAAQ;AAC1D,OACE,OAAO,WAAW,YAClB,WAAW,QACX,WAAW,UACX,MAAM,QAAS,OAA8B,MAAM,CAEnD,QAAQ,OAAgC;AAE1C,OAAI,MAAM,QAAQ,OAAO,CACvB,QAAO;AAET,SAAM,IAAI,MAAM,sCAAsC;;EAGxD,MAAM,aAAa,aAAsC;GACvD,MAAM,SAAS,YAAY,SAAS,SAAS;GAE7C,IAAI,YAAY;AAChB,OAAI,YAAY,OAAO,OAAQ,YAAY,OAAO,GAAM,aAAY;YAC3D,YAAY,OAAO,MAAQ,YAAY,OAAO,GAAM,aAAY;YAChE,YAAY,OAAO,MAAQ,YAAY,OAAO,GAAM,aAAY;GAqBzE,MAAM,UADO,MAAM,QAhBe,CAChC;IACE,MAAM;IACN,SAAS,CACP;KACE,MAAM;KACN,WAAW,EAAE,KARL,QAAQ,UAAU,UAAU,UAQT;KAC5B,EACD;KACE,MAAM;KACN,MAAM;KACP,CACF;IACF,CACF,EAEoC,MAAM,EACvB,QAAQ;AAC5B,OAAI,CAAC,UAAU,OAAO,QAAQ,YAAY,KACxC,OAAM,IAAI,MAAM,+CAA+C;AAEjE,UAAO,OAAO,QAAQ,QAAQ,MAAM;;EAEvC;;;;;ACnHH,SAAgB,eAAe,QAAoC;AACjE,SAAQ,OAAO,UAAf;EACE,KAAK,YACH,QAAO,wBAAwB,OAAO,OAAO;EAC/C,KAAK,SACH,QAAO,qBAAqB,OAAO,OAAO;EAC5C,SAAS;GACP,MAAM,cAAqB,OAAO;AAClC,SAAM,IAAI,MAAM,qBAAqB,OAAO,YAAY,GAAG"}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
+
//#region src/core/prompt-builder.ts
|
|
3
|
+
function describeField(field, existingIds) {
|
|
4
|
+
const lines = [];
|
|
5
|
+
const required = field.required ? " (required)" : " (optional)";
|
|
6
|
+
switch (field.type) {
|
|
7
|
+
case "text":
|
|
8
|
+
case "textarea":
|
|
9
|
+
case "email":
|
|
10
|
+
lines.push(`- "${field.name}" (${field.type}${required}): Generate realistic ${field.type} content`);
|
|
11
|
+
break;
|
|
12
|
+
case "number":
|
|
13
|
+
lines.push(`- "${field.name}" (number${required}): Generate a realistic numeric value (e.g. price 1–999, quantity 1–100, rating 1–5)`);
|
|
14
|
+
break;
|
|
15
|
+
case "checkbox":
|
|
16
|
+
lines.push(`- "${field.name}" (boolean${required}): true or false`);
|
|
17
|
+
break;
|
|
18
|
+
case "date":
|
|
19
|
+
lines.push(`- "${field.name}" (date${required}): ISO 8601 date string (e.g. "2024-06-15T10:00:00.000Z")`);
|
|
20
|
+
break;
|
|
21
|
+
case "select": {
|
|
22
|
+
const values = (field.options ?? []).map((o) => `"${o.value}"`).join(", ");
|
|
23
|
+
lines.push(`- "${field.name}" (select${required}): Must be one of [${values}]`);
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
case "relationship": {
|
|
27
|
+
const collections = Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo ?? ""];
|
|
28
|
+
const idLists = collections.map((col) => {
|
|
29
|
+
const ids = existingIds?.[col] ?? [];
|
|
30
|
+
return ids.length > 0 ? `${col}: [${ids.map((id) => `"${id}"`).join(", ")}]` : `${col}: (no existing IDs available — omit this field)`;
|
|
31
|
+
});
|
|
32
|
+
if (collections.some((col) => (existingIds?.[col] ?? []).length > 0)) lines.push(`- "${field.name}" (relationship${required}): Pick from existing IDs — ${idLists.join("; ")}${field.hasMany ? " (can be an array of IDs)" : " (single ID string)"}`);
|
|
33
|
+
else lines.push(`- "${field.name}" (relationship${required}): SKIP — no existing IDs available`);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case "richText":
|
|
37
|
+
lines.push(`- "${field.name}" (richtext${required}): Return PLAIN TEXT only (do not wrap in Lexical/JSON — the system will convert it). Write 1–3 sentences of realistic content.`);
|
|
38
|
+
break;
|
|
39
|
+
case "upload":
|
|
40
|
+
lines.push(`- "${field.name}" (upload${required}): SKIP — handled separately`);
|
|
41
|
+
break;
|
|
42
|
+
case "array":
|
|
43
|
+
case "group":
|
|
44
|
+
if (field.fields && field.fields.length > 0) {
|
|
45
|
+
lines.push(`- "${field.name}" (${field.type}${required}):`);
|
|
46
|
+
for (const subField of field.fields) lines.push(` ${describeField(subField, existingIds)}`);
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
case "blocks":
|
|
50
|
+
lines.push(`- "${field.name}" (blocks${required}): SKIP — complex layout field, omit`);
|
|
51
|
+
break;
|
|
52
|
+
default: lines.push(`- "${field.name}" (${field.type}${required}): Generate appropriate content`);
|
|
53
|
+
}
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
function buildGenerationPrompt(schema, context) {
|
|
57
|
+
const fieldDescriptions = schema.fields.map((f) => describeField(f, context.existingIds)).join("\n");
|
|
58
|
+
const requiredNote = schema.requiredFields.length > 0 ? `\nRequired fields (must be present): ${schema.requiredFields.map((f) => `"${f}"`).join(", ")}` : "";
|
|
59
|
+
const themeNote = context.theme ? `\nContent theme/style: ${context.theme}` : "";
|
|
60
|
+
const localeNote = context.locale && context.locale !== "en" ? `\nGenerate content in locale: ${context.locale}` : "";
|
|
61
|
+
return `Generate ${context.count} realistic document(s) for the "${schema.slug}" collection.
|
|
62
|
+
${themeNote}${localeNote}${requiredNote}
|
|
63
|
+
|
|
64
|
+
Fields to generate:
|
|
65
|
+
${fieldDescriptions}
|
|
66
|
+
|
|
67
|
+
Rules:
|
|
68
|
+
- Return a JSON array with exactly ${context.count} item(s)
|
|
69
|
+
- Each item must be a flat JSON object with field names as keys
|
|
70
|
+
- Skip fields marked as SKIP
|
|
71
|
+
- For richtext fields: return plain text strings only
|
|
72
|
+
- For relationship fields: use the provided existing IDs exactly as shown
|
|
73
|
+
- Do not include extra fields not listed above
|
|
74
|
+
- Generate varied, realistic content appropriate for an ecommerce platform`;
|
|
75
|
+
}
|
|
76
|
+
function buildOutputSchema(schema) {
|
|
77
|
+
const properties = {};
|
|
78
|
+
const required = [];
|
|
79
|
+
for (const field of schema.fields) {
|
|
80
|
+
if ([
|
|
81
|
+
"relationship",
|
|
82
|
+
"richText",
|
|
83
|
+
"upload",
|
|
84
|
+
"blocks"
|
|
85
|
+
].includes(field.type)) continue;
|
|
86
|
+
let fieldSchema;
|
|
87
|
+
switch (field.type) {
|
|
88
|
+
case "text":
|
|
89
|
+
case "textarea":
|
|
90
|
+
case "email":
|
|
91
|
+
case "date":
|
|
92
|
+
fieldSchema = { type: "string" };
|
|
93
|
+
break;
|
|
94
|
+
case "number":
|
|
95
|
+
fieldSchema = { type: "number" };
|
|
96
|
+
break;
|
|
97
|
+
case "checkbox":
|
|
98
|
+
fieldSchema = { type: "boolean" };
|
|
99
|
+
break;
|
|
100
|
+
case "select":
|
|
101
|
+
fieldSchema = {
|
|
102
|
+
type: "string",
|
|
103
|
+
enum: (field.options ?? []).map((o) => o.value)
|
|
104
|
+
};
|
|
105
|
+
break;
|
|
106
|
+
case "array":
|
|
107
|
+
fieldSchema = {
|
|
108
|
+
type: "array",
|
|
109
|
+
items: { type: "object" }
|
|
110
|
+
};
|
|
111
|
+
break;
|
|
112
|
+
case "group":
|
|
113
|
+
fieldSchema = { type: "object" };
|
|
114
|
+
break;
|
|
115
|
+
default: fieldSchema = { type: "string" };
|
|
116
|
+
}
|
|
117
|
+
properties[field.name] = fieldSchema;
|
|
118
|
+
if (field.required) required.push(field.name);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: { items: {
|
|
123
|
+
type: "array",
|
|
124
|
+
items: {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties,
|
|
127
|
+
required
|
|
128
|
+
}
|
|
129
|
+
} }
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/core/content-generator.ts
|
|
134
|
+
var content_generator_exports = /* @__PURE__ */ __exportAll({ generateDocuments: () => generateDocuments });
|
|
135
|
+
function getSelectValues(field) {
|
|
136
|
+
return (field.options ?? []).map((o) => o.value);
|
|
137
|
+
}
|
|
138
|
+
function validateDocument(doc, schema) {
|
|
139
|
+
const errors = [];
|
|
140
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return [{
|
|
141
|
+
field: "_root",
|
|
142
|
+
message: "Document must be a plain object"
|
|
143
|
+
}];
|
|
144
|
+
const record = doc;
|
|
145
|
+
for (const fieldName of schema.requiredFields) {
|
|
146
|
+
const field = schema.fields.find((f) => f.name === fieldName);
|
|
147
|
+
if (!field) continue;
|
|
148
|
+
if ([
|
|
149
|
+
"relationship",
|
|
150
|
+
"richText",
|
|
151
|
+
"upload",
|
|
152
|
+
"blocks"
|
|
153
|
+
].includes(field.type)) continue;
|
|
154
|
+
if (record[fieldName] === void 0 || record[fieldName] === null || record[fieldName] === "") errors.push({
|
|
155
|
+
field: fieldName,
|
|
156
|
+
message: `Required field "${fieldName}" is missing or empty`
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
for (const field of schema.fields) {
|
|
160
|
+
if (field.type !== "select") continue;
|
|
161
|
+
const value = record[field.name];
|
|
162
|
+
if (value === void 0 || value === null) continue;
|
|
163
|
+
const validValues = getSelectValues(field);
|
|
164
|
+
if (validValues.length > 0 && !validValues.includes(String(value))) errors.push({
|
|
165
|
+
field: field.name,
|
|
166
|
+
message: `Field "${field.name}" has invalid select value "${String(value)}". Must be one of: ${validValues.join(", ")}`
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return errors;
|
|
170
|
+
}
|
|
171
|
+
function buildRetryPrompt(originalPrompt, errors) {
|
|
172
|
+
return `${originalPrompt}
|
|
173
|
+
|
|
174
|
+
IMPORTANT: Your previous response had validation errors. Fix these issues:
|
|
175
|
+
${errors.map((e) => `- ${e.field}: ${e.message}`).join("\n")}
|
|
176
|
+
|
|
177
|
+
Return a corrected JSON array addressing all the errors above.`;
|
|
178
|
+
}
|
|
179
|
+
async function generateDocuments(provider, schema, context, options) {
|
|
180
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
181
|
+
const outputSchema = buildOutputSchema(schema);
|
|
182
|
+
let prompt = buildGenerationPrompt(schema, context);
|
|
183
|
+
let lastError = null;
|
|
184
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
185
|
+
let rawItems;
|
|
186
|
+
try {
|
|
187
|
+
rawItems = await provider.generate(prompt, outputSchema);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
190
|
+
if (attempt < maxRetries) {
|
|
191
|
+
prompt = buildRetryPrompt(buildGenerationPrompt(schema, context), [{
|
|
192
|
+
field: "_generation",
|
|
193
|
+
message: `API error: ${lastError.message}`
|
|
194
|
+
}]);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
throw lastError;
|
|
198
|
+
}
|
|
199
|
+
const allErrors = [];
|
|
200
|
+
const validDocuments = [];
|
|
201
|
+
for (const item of rawItems) {
|
|
202
|
+
const errors = validateDocument(item, schema);
|
|
203
|
+
if (errors.length > 0) allErrors.push(...errors);
|
|
204
|
+
else validDocuments.push(item);
|
|
205
|
+
}
|
|
206
|
+
if (allErrors.length === 0) return { documents: validDocuments };
|
|
207
|
+
if (attempt < maxRetries) {
|
|
208
|
+
prompt = buildRetryPrompt(buildGenerationPrompt(schema, context), allErrors);
|
|
209
|
+
lastError = /* @__PURE__ */ new Error(`Validation failed: ${allErrors.map((e) => e.message).join("; ")}`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (validDocuments.length > 0) return { documents: validDocuments };
|
|
213
|
+
throw new Error(`Content generation failed after ${maxRetries} retries. Last errors: ${allErrors.map((e) => e.message).join("; ")}`);
|
|
214
|
+
}
|
|
215
|
+
throw lastError ?? /* @__PURE__ */ new Error("Content generation failed");
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
export { generateDocuments as n, content_generator_exports as t };
|
|
219
|
+
|
|
220
|
+
//# sourceMappingURL=content-generator-fPX2DL3g.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-generator-fPX2DL3g.mjs","names":[],"sources":["../src/core/prompt-builder.ts","../src/core/content-generator.ts"],"sourcesContent":["import type { CollectionSchema, FieldSchema } from '../types.js'\n\nexport type GenerationContext = {\n count: number\n theme?: string\n locale?: string\n existingIds?: Record<string, string[]>\n}\n\nfunction describeField(field: FieldSchema, existingIds?: Record<string, string[]>): string {\n const lines: string[] = []\n const required = field.required ? ' (required)' : ' (optional)'\n\n switch (field.type) {\n case 'text':\n case 'textarea':\n case 'email':\n lines.push(\n `- \"${field.name}\" (${field.type}${required}): Generate realistic ${field.type} content`,\n )\n break\n\n case 'number':\n lines.push(\n `- \"${field.name}\" (number${required}): Generate a realistic numeric value (e.g. price 1–999, quantity 1–100, rating 1–5)`,\n )\n break\n\n case 'checkbox':\n lines.push(`- \"${field.name}\" (boolean${required}): true or false`)\n break\n\n case 'date':\n lines.push(\n `- \"${field.name}\" (date${required}): ISO 8601 date string (e.g. \"2024-06-15T10:00:00.000Z\")`,\n )\n break\n\n case 'select': {\n const values = (field.options ?? []).map((o) => `\"${o.value}\"`).join(', ')\n lines.push(`- \"${field.name}\" (select${required}): Must be one of [${values}]`)\n break\n }\n\n case 'relationship': {\n const collections = Array.isArray(field.relationTo)\n ? field.relationTo\n : [field.relationTo ?? '']\n const idLists = collections.map((col) => {\n const ids = existingIds?.[col] ?? []\n return ids.length > 0\n ? `${col}: [${ids.map((id) => `\"${id}\"`).join(', ')}]`\n : `${col}: (no existing IDs available — omit this field)`\n })\n const hasIds = collections.some((col) => (existingIds?.[col] ?? []).length > 0)\n if (hasIds) {\n lines.push(\n `- \"${field.name}\" (relationship${required}): Pick from existing IDs — ${idLists.join('; ')}${field.hasMany ? ' (can be an array of IDs)' : ' (single ID string)'}`,\n )\n } else {\n lines.push(`- \"${field.name}\" (relationship${required}): SKIP — no existing IDs available`)\n }\n break\n }\n\n case 'richText':\n lines.push(\n `- \"${field.name}\" (richtext${required}): Return PLAIN TEXT only (do not wrap in Lexical/JSON — the system will convert it). Write 1–3 sentences of realistic content.`,\n )\n break\n\n case 'upload':\n lines.push(`- \"${field.name}\" (upload${required}): SKIP — handled separately`)\n break\n\n case 'array':\n case 'group':\n if (field.fields && field.fields.length > 0) {\n lines.push(`- \"${field.name}\" (${field.type}${required}):`)\n for (const subField of field.fields) {\n lines.push(` ${describeField(subField, existingIds)}`)\n }\n }\n break\n\n case 'blocks':\n lines.push(`- \"${field.name}\" (blocks${required}): SKIP — complex layout field, omit`)\n break\n\n default:\n lines.push(`- \"${field.name}\" (${field.type}${required}): Generate appropriate content`)\n }\n\n return lines.join('\\n')\n}\n\nexport function buildGenerationPrompt(\n schema: CollectionSchema,\n context: GenerationContext,\n): string {\n const fieldDescriptions = schema.fields\n .map((f) => describeField(f, context.existingIds))\n .join('\\n')\n\n const requiredNote =\n schema.requiredFields.length > 0\n ? `\\nRequired fields (must be present): ${schema.requiredFields.map((f) => `\"${f}\"`).join(', ')}`\n : ''\n\n const themeNote = context.theme ? `\\nContent theme/style: ${context.theme}` : ''\n\n const localeNote =\n context.locale && context.locale !== 'en'\n ? `\\nGenerate content in locale: ${context.locale}`\n : ''\n\n return `Generate ${context.count} realistic document(s) for the \"${schema.slug}\" collection.\n${themeNote}${localeNote}${requiredNote}\n\nFields to generate:\n${fieldDescriptions}\n\nRules:\n- Return a JSON array with exactly ${context.count} item(s)\n- Each item must be a flat JSON object with field names as keys\n- Skip fields marked as SKIP\n- For richtext fields: return plain text strings only\n- For relationship fields: use the provided existing IDs exactly as shown\n- Do not include extra fields not listed above\n- Generate varied, realistic content appropriate for an ecommerce platform`\n}\n\nexport function buildOutputSchema(schema: CollectionSchema): Record<string, unknown> {\n const properties: Record<string, unknown> = {}\n const required: string[] = []\n\n for (const field of schema.fields) {\n // Skip fields handled separately\n if (['relationship', 'richText', 'upload', 'blocks'].includes(field.type)) {\n continue\n }\n\n let fieldSchema: Record<string, unknown>\n\n switch (field.type) {\n case 'text':\n case 'textarea':\n case 'email':\n case 'date':\n fieldSchema = { type: 'string' }\n break\n\n case 'number':\n fieldSchema = { type: 'number' }\n break\n\n case 'checkbox':\n fieldSchema = { type: 'boolean' }\n break\n\n case 'select': {\n const enumValues = (field.options ?? []).map((o) => o.value)\n fieldSchema = { type: 'string', enum: enumValues }\n break\n }\n\n case 'array':\n fieldSchema = { type: 'array', items: { type: 'object' } }\n break\n\n case 'group':\n fieldSchema = { type: 'object' }\n break\n\n default:\n fieldSchema = { type: 'string' }\n }\n\n properties[field.name] = fieldSchema\n if (field.required) {\n required.push(field.name)\n }\n }\n\n return {\n type: 'object',\n properties: {\n items: {\n type: 'array',\n items: {\n type: 'object',\n properties,\n required,\n },\n },\n },\n }\n}\n","import type { AIProvider, CollectionSchema, FieldSchema } from '../types.js'\nimport type { GenerationContext } from './prompt-builder.js'\nimport { buildGenerationPrompt, buildOutputSchema } from './prompt-builder.js'\n\nexport type { GenerationContext }\n\nexport type GenerationResult = {\n documents: Record<string, unknown>[]\n tokensUsed?: number\n}\n\ntype ValidationError = {\n field: string\n message: string\n}\n\nfunction getSelectValues(field: FieldSchema): string[] {\n return (field.options ?? []).map((o) => o.value)\n}\n\nfunction validateDocument(doc: unknown, schema: CollectionSchema): ValidationError[] {\n const errors: ValidationError[] = []\n\n if (typeof doc !== 'object' || doc === null || Array.isArray(doc)) {\n return [{ field: '_root', message: 'Document must be a plain object' }]\n }\n\n const record = doc as Record<string, unknown>\n\n // Check required fields\n for (const fieldName of schema.requiredFields) {\n const field = schema.fields.find((f) => f.name === fieldName)\n if (!field) continue\n // Skip fields that are handled separately\n if (['relationship', 'richText', 'upload', 'blocks'].includes(field.type)) continue\n if (record[fieldName] === undefined || record[fieldName] === null || record[fieldName] === '') {\n errors.push({\n field: fieldName,\n message: `Required field \"${fieldName}\" is missing or empty`,\n })\n }\n }\n\n // Validate select field values\n for (const field of schema.fields) {\n if (field.type !== 'select') continue\n const value = record[field.name]\n if (value === undefined || value === null) continue\n const validValues = getSelectValues(field)\n if (validValues.length > 0 && !validValues.includes(String(value))) {\n errors.push({\n field: field.name,\n message: `Field \"${field.name}\" has invalid select value \"${String(value)}\". Must be one of: ${validValues.join(', ')}`,\n })\n }\n }\n\n return errors\n}\n\nfunction buildRetryPrompt(originalPrompt: string, errors: ValidationError[]): string {\n const errorList = errors.map((e) => `- ${e.field}: ${e.message}`).join('\\n')\n return `${originalPrompt}\n\nIMPORTANT: Your previous response had validation errors. Fix these issues:\n${errorList}\n\nReturn a corrected JSON array addressing all the errors above.`\n}\n\nexport async function generateDocuments(\n provider: AIProvider,\n schema: CollectionSchema,\n context: GenerationContext,\n options?: { maxRetries?: number },\n): Promise<GenerationResult> {\n const maxRetries = options?.maxRetries ?? 3\n const outputSchema = buildOutputSchema(schema)\n\n let prompt = buildGenerationPrompt(schema, context)\n let lastError: Error | null = null\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n let rawItems: unknown[]\n\n try {\n rawItems = await provider.generate(prompt, outputSchema)\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err))\n if (attempt < maxRetries) {\n prompt = buildRetryPrompt(buildGenerationPrompt(schema, context), [\n { field: '_generation', message: `API error: ${lastError.message}` },\n ])\n continue\n }\n throw lastError\n }\n\n // Validate all documents\n const allErrors: ValidationError[] = []\n const validDocuments: Record<string, unknown>[] = []\n\n for (const item of rawItems) {\n const errors = validateDocument(item, schema)\n if (errors.length > 0) {\n allErrors.push(...errors)\n } else {\n validDocuments.push(item as Record<string, unknown>)\n }\n }\n\n if (allErrors.length === 0) {\n return { documents: validDocuments }\n }\n\n // Validation failed — retry if attempts remain\n if (attempt < maxRetries) {\n prompt = buildRetryPrompt(buildGenerationPrompt(schema, context), allErrors)\n lastError = new Error(`Validation failed: ${allErrors.map((e) => e.message).join('; ')}`)\n continue\n }\n\n // Out of retries — return what we have if any valid docs, otherwise throw\n if (validDocuments.length > 0) {\n return { documents: validDocuments }\n }\n\n throw new Error(\n `Content generation failed after ${maxRetries} retries. Last errors: ${allErrors.map((e) => e.message).join('; ')}`,\n )\n }\n\n throw lastError ?? new Error('Content generation failed')\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//#region src/core/dependency-resolver.ts
|
|
2
|
+
/**
|
|
3
|
+
* Returns the direct dependency slugs for a collection schema.
|
|
4
|
+
* Dependencies are collections that must be created before this one.
|
|
5
|
+
* Self-referential relationships are excluded (handled via deferred updates).
|
|
6
|
+
*/
|
|
7
|
+
function getDependencies(schema) {
|
|
8
|
+
const deps = /* @__PURE__ */ new Set();
|
|
9
|
+
for (const rel of schema.relationships) {
|
|
10
|
+
if (rel.isSelfReferential) continue;
|
|
11
|
+
const targets = Array.isArray(rel.collection) ? rel.collection : [rel.collection];
|
|
12
|
+
for (const target of targets) if (target !== schema.slug) deps.add(target);
|
|
13
|
+
}
|
|
14
|
+
return [...deps];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Topologically sorts collection schemas using Kahn's algorithm.
|
|
18
|
+
* Returns slugs in creation order (dependencies first).
|
|
19
|
+
* Circular dependencies (excluding self-referential) are broken arbitrarily
|
|
20
|
+
* and a warning is emitted.
|
|
21
|
+
*/
|
|
22
|
+
function resolveCreationOrder(schemas) {
|
|
23
|
+
const slugSet = new Set(schemas.map((s) => s.slug));
|
|
24
|
+
const deps = /* @__PURE__ */ new Map();
|
|
25
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
26
|
+
for (const schema of schemas) {
|
|
27
|
+
if (!deps.has(schema.slug)) deps.set(schema.slug, /* @__PURE__ */ new Set());
|
|
28
|
+
if (!dependents.has(schema.slug)) dependents.set(schema.slug, /* @__PURE__ */ new Set());
|
|
29
|
+
for (const dep of getDependencies(schema)) {
|
|
30
|
+
if (!slugSet.has(dep)) continue;
|
|
31
|
+
deps.get(schema.slug)?.add(dep);
|
|
32
|
+
if (!dependents.has(dep)) dependents.set(dep, /* @__PURE__ */ new Set());
|
|
33
|
+
dependents.get(dep)?.add(schema.slug);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
37
|
+
for (const schema of schemas) inDegree.set(schema.slug, deps.get(schema.slug)?.size ?? 0);
|
|
38
|
+
const queue = [];
|
|
39
|
+
for (const [slug, degree] of inDegree) if (degree === 0) queue.push(slug);
|
|
40
|
+
queue.sort();
|
|
41
|
+
const order = [];
|
|
42
|
+
while (queue.length > 0) {
|
|
43
|
+
queue.sort();
|
|
44
|
+
const slug = queue.shift();
|
|
45
|
+
order.push(slug);
|
|
46
|
+
for (const dependent of dependents.get(slug) ?? []) {
|
|
47
|
+
const newDegree = (inDegree.get(dependent) ?? 1) - 1;
|
|
48
|
+
inDegree.set(dependent, newDegree);
|
|
49
|
+
if (newDegree === 0) queue.push(dependent);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (order.length < schemas.length) {
|
|
53
|
+
const remaining = schemas.map((s) => s.slug).filter((s) => !order.includes(s));
|
|
54
|
+
console.warn(`[dependency-resolver] Circular dependency detected among: ${remaining.join(", ")}. These will be appended in arbitrary order.`);
|
|
55
|
+
order.push(...remaining.sort());
|
|
56
|
+
}
|
|
57
|
+
return order;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { resolveCreationOrder };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=dependency-resolver-BgsUvfRJ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dependency-resolver-BgsUvfRJ.mjs","names":[],"sources":["../src/core/dependency-resolver.ts"],"sourcesContent":["import type { CollectionSchema } from '../types.js'\n\n/**\n * Returns the direct dependency slugs for a collection schema.\n * Dependencies are collections that must be created before this one.\n * Self-referential relationships are excluded (handled via deferred updates).\n */\nexport function getDependencies(schema: CollectionSchema): string[] {\n const deps = new Set<string>()\n\n for (const rel of schema.relationships) {\n if (rel.isSelfReferential) continue\n\n const targets = Array.isArray(rel.collection) ? rel.collection : [rel.collection]\n for (const target of targets) {\n if (target !== schema.slug) {\n deps.add(target)\n }\n }\n }\n\n return [...deps]\n}\n\n/**\n * Topologically sorts collection schemas using Kahn's algorithm.\n * Returns slugs in creation order (dependencies first).\n * Circular dependencies (excluding self-referential) are broken arbitrarily\n * and a warning is emitted.\n */\nexport function resolveCreationOrder(schemas: CollectionSchema[]): string[] {\n const slugSet = new Set(schemas.map((s) => s.slug))\n\n // Build adjacency list: slug → set of slugs it depends on (that are in our schema set)\n const deps = new Map<string, Set<string>>()\n // Reverse: slug → set of slugs that depend on it\n const dependents = new Map<string, Set<string>>()\n\n for (const schema of schemas) {\n if (!deps.has(schema.slug)) deps.set(schema.slug, new Set())\n if (!dependents.has(schema.slug)) dependents.set(schema.slug, new Set())\n\n for (const dep of getDependencies(schema)) {\n // Only track dependencies within the provided schema set\n if (!slugSet.has(dep)) continue\n deps.get(schema.slug)?.add(dep)\n\n if (!dependents.has(dep)) dependents.set(dep, new Set())\n dependents.get(dep)?.add(schema.slug)\n }\n }\n\n // Kahn's algorithm\n const inDegree = new Map<string, number>()\n for (const schema of schemas) {\n inDegree.set(schema.slug, deps.get(schema.slug)?.size ?? 0)\n }\n\n const queue: string[] = []\n for (const [slug, degree] of inDegree) {\n if (degree === 0) queue.push(slug)\n }\n // Sort for deterministic output\n queue.sort()\n\n const order: string[] = []\n\n while (queue.length > 0) {\n // Sort queue for determinism each iteration\n queue.sort()\n const slug = queue.shift()!\n order.push(slug)\n\n for (const dependent of dependents.get(slug) ?? []) {\n const newDegree = (inDegree.get(dependent) ?? 1) - 1\n inDegree.set(dependent, newDegree)\n if (newDegree === 0) {\n queue.push(dependent)\n }\n }\n }\n\n // If we didn't include everything, there are circular dependencies\n if (order.length < schemas.length) {\n const remaining = schemas.map((s) => s.slug).filter((s) => !order.includes(s))\n console.warn(\n `[dependency-resolver] Circular dependency detected among: ${remaining.join(', ')}. ` +\n 'These will be appended in arbitrary order.',\n )\n order.push(...remaining.sort())\n }\n\n return order\n}\n"],"mappings":";;;;;;AAOA,SAAgB,gBAAgB,QAAoC;CAClE,MAAM,uBAAO,IAAI,KAAa;AAE9B,MAAK,MAAM,OAAO,OAAO,eAAe;AACtC,MAAI,IAAI,kBAAmB;EAE3B,MAAM,UAAU,MAAM,QAAQ,IAAI,WAAW,GAAG,IAAI,aAAa,CAAC,IAAI,WAAW;AACjF,OAAK,MAAM,UAAU,QACnB,KAAI,WAAW,OAAO,KACpB,MAAK,IAAI,OAAO;;AAKtB,QAAO,CAAC,GAAG,KAAK;;;;;;;;AASlB,SAAgB,qBAAqB,SAAuC;CAC1E,MAAM,UAAU,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,KAAK,CAAC;CAGnD,MAAM,uBAAO,IAAI,KAA0B;CAE3C,MAAM,6BAAa,IAAI,KAA0B;AAEjD,MAAK,MAAM,UAAU,SAAS;AAC5B,MAAI,CAAC,KAAK,IAAI,OAAO,KAAK,CAAE,MAAK,IAAI,OAAO,sBAAM,IAAI,KAAK,CAAC;AAC5D,MAAI,CAAC,WAAW,IAAI,OAAO,KAAK,CAAE,YAAW,IAAI,OAAO,sBAAM,IAAI,KAAK,CAAC;AAExE,OAAK,MAAM,OAAO,gBAAgB,OAAO,EAAE;AAEzC,OAAI,CAAC,QAAQ,IAAI,IAAI,CAAE;AACvB,QAAK,IAAI,OAAO,KAAK,EAAE,IAAI,IAAI;AAE/B,OAAI,CAAC,WAAW,IAAI,IAAI,CAAE,YAAW,IAAI,qBAAK,IAAI,KAAK,CAAC;AACxD,cAAW,IAAI,IAAI,EAAE,IAAI,OAAO,KAAK;;;CAKzC,MAAM,2BAAW,IAAI,KAAqB;AAC1C,MAAK,MAAM,UAAU,QACnB,UAAS,IAAI,OAAO,MAAM,KAAK,IAAI,OAAO,KAAK,EAAE,QAAQ,EAAE;CAG7D,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,CAAC,MAAM,WAAW,SAC3B,KAAI,WAAW,EAAG,OAAM,KAAK,KAAK;AAGpC,OAAM,MAAM;CAEZ,MAAM,QAAkB,EAAE;AAE1B,QAAO,MAAM,SAAS,GAAG;AAEvB,QAAM,MAAM;EACZ,MAAM,OAAO,MAAM,OAAO;AAC1B,QAAM,KAAK,KAAK;AAEhB,OAAK,MAAM,aAAa,WAAW,IAAI,KAAK,IAAI,EAAE,EAAE;GAClD,MAAM,aAAa,SAAS,IAAI,UAAU,IAAI,KAAK;AACnD,YAAS,IAAI,WAAW,UAAU;AAClC,OAAI,cAAc,EAChB,OAAM,KAAK,UAAU;;;AAM3B,KAAI,MAAM,SAAS,QAAQ,QAAQ;EACjC,MAAM,YAAY,QAAQ,KAAK,MAAM,EAAE,KAAK,CAAC,QAAQ,MAAM,CAAC,MAAM,SAAS,EAAE,CAAC;AAC9E,UAAQ,KACN,6DAA6D,UAAU,KAAK,KAAK,CAAC,8CAEnF;AACD,QAAM,KAAK,GAAG,UAAU,MAAM,CAAC;;AAGjC,QAAO"}
|