ai-database 2.1.3 → 2.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/CHANGELOG.md +35 -1
- package/README.md +880 -669
- package/dist/actions.d.ts +2 -2
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +1 -1
- package/dist/actions.js.map +1 -1
- package/dist/ai-promise-db.d.ts +49 -23
- package/dist/ai-promise-db.d.ts.map +1 -1
- package/dist/ai-promise-db.js +91 -63
- package/dist/ai-promise-db.js.map +1 -1
- package/dist/authorization.d.ts.map +1 -1
- package/dist/authorization.js +38 -30
- package/dist/authorization.js.map +1 -1
- package/dist/cascade-orchestrator.d.ts +404 -0
- package/dist/cascade-orchestrator.d.ts.map +1 -0
- package/dist/cascade-orchestrator.js +828 -0
- package/dist/cascade-orchestrator.js.map +1 -0
- package/dist/cascade-write-strategy.d.ts +584 -0
- package/dist/cascade-write-strategy.d.ts.map +1 -0
- package/dist/cascade-write-strategy.js +590 -0
- package/dist/cascade-write-strategy.js.map +1 -0
- package/dist/ch-adapter.d.ts +358 -0
- package/dist/ch-adapter.d.ts.map +1 -0
- package/dist/ch-adapter.js +929 -0
- package/dist/ch-adapter.js.map +1 -0
- package/dist/client/index.d.ts +42 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +43 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client.d.ts +266 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +81 -0
- package/dist/client.js.map +1 -0
- package/dist/constants.d.ts +64 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +52 -2
- package/dist/constants.js.map +1 -1
- package/dist/dataloader.d.ts +99 -0
- package/dist/dataloader.d.ts.map +1 -0
- package/dist/dataloader.js +225 -0
- package/dist/dataloader.js.map +1 -0
- package/dist/db-provider-port.d.ts +501 -0
- package/dist/db-provider-port.d.ts.map +1 -0
- package/dist/db-provider-port.js +113 -0
- package/dist/db-provider-port.js.map +1 -0
- package/dist/digital-objects-provider.d.ts +49 -0
- package/dist/digital-objects-provider.d.ts.map +1 -0
- package/dist/digital-objects-provider.js +55 -0
- package/dist/digital-objects-provider.js.map +1 -0
- package/dist/do-sqlite-adapter.d.ts +402 -0
- package/dist/do-sqlite-adapter.d.ts.map +1 -0
- package/dist/do-sqlite-adapter.js +745 -0
- package/dist/do-sqlite-adapter.js.map +1 -0
- package/dist/docs-rels/custom-types.d.ts +134 -0
- package/dist/docs-rels/custom-types.d.ts.map +1 -0
- package/dist/docs-rels/custom-types.js +70 -0
- package/dist/docs-rels/custom-types.js.map +1 -0
- package/dist/docs-rels/index.d.ts +16 -0
- package/dist/docs-rels/index.d.ts.map +1 -0
- package/dist/docs-rels/index.js +16 -0
- package/dist/docs-rels/index.js.map +1 -0
- package/dist/docs-rels/migrations/index.d.ts +30 -0
- package/dist/docs-rels/migrations/index.d.ts.map +1 -0
- package/dist/docs-rels/migrations/index.js +128 -0
- package/dist/docs-rels/migrations/index.js.map +1 -0
- package/dist/docs-rels/schema.d.ts +2961 -0
- package/dist/docs-rels/schema.d.ts.map +1 -0
- package/dist/docs-rels/schema.js +244 -0
- package/dist/docs-rels/schema.js.map +1 -0
- package/dist/durable-clickhouse.d.ts.map +1 -1
- package/dist/durable-clickhouse.js +16 -13
- package/dist/durable-clickhouse.js.map +1 -1
- package/dist/durable-promise.d.ts.map +1 -1
- package/dist/durable-promise.js +34 -15
- package/dist/durable-promise.js.map +1 -1
- package/dist/errors.d.ts +127 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +210 -0
- package/dist/errors.js.map +1 -0
- package/dist/eventbridge.d.ts +117 -0
- package/dist/eventbridge.d.ts.map +1 -0
- package/dist/eventbridge.js +238 -0
- package/dist/eventbridge.js.map +1 -0
- package/dist/events.d.ts +2 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +1 -1
- package/dist/events.js.map +1 -1
- package/dist/execution-queue.d.ts.map +1 -1
- package/dist/execution-queue.js +4 -5
- package/dist/execution-queue.js.map +1 -1
- package/dist/index.d.ts +35 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +106 -6
- package/dist/index.js.map +1 -1
- package/dist/linguistic.d.ts +3 -108
- package/dist/linguistic.d.ts.map +1 -1
- package/dist/linguistic.js +3 -372
- package/dist/linguistic.js.map +1 -1
- package/dist/logger.d.ts +132 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +137 -0
- package/dist/logger.js.map +1 -0
- package/dist/memory-provider.d.ts +128 -0
- package/dist/memory-provider.d.ts.map +1 -1
- package/dist/memory-provider.js +592 -257
- package/dist/memory-provider.js.map +1 -1
- package/dist/pg-adapter.d.ts +424 -0
- package/dist/pg-adapter.d.ts.map +1 -0
- package/dist/pg-adapter.js +921 -0
- package/dist/pg-adapter.js.map +1 -0
- package/dist/pipelines-iceberg-emitter.d.ts +327 -0
- package/dist/pipelines-iceberg-emitter.d.ts.map +1 -0
- package/dist/pipelines-iceberg-emitter.js +351 -0
- package/dist/pipelines-iceberg-emitter.js.map +1 -0
- package/dist/provider-capabilities.d.ts +146 -0
- package/dist/provider-capabilities.d.ts.map +1 -0
- package/dist/provider-capabilities.js +214 -0
- package/dist/provider-capabilities.js.map +1 -0
- package/dist/rdb-provider-adapter.d.ts +195 -0
- package/dist/rdb-provider-adapter.d.ts.map +1 -0
- package/dist/rdb-provider-adapter.js +291 -0
- package/dist/rdb-provider-adapter.js.map +1 -0
- package/dist/schema/cascade.d.ts +48 -17
- package/dist/schema/cascade.d.ts.map +1 -1
- package/dist/schema/cascade.js +477 -278
- package/dist/schema/cascade.js.map +1 -1
- package/dist/schema/definition-caches.d.ts +24 -0
- package/dist/schema/definition-caches.d.ts.map +1 -0
- package/dist/schema/definition-caches.js +26 -0
- package/dist/schema/definition-caches.js.map +1 -0
- package/dist/schema/dependency-graph.d.ts +21 -109
- package/dist/schema/dependency-graph.d.ts.map +1 -1
- package/dist/schema/dependency-graph.js +25 -333
- package/dist/schema/dependency-graph.js.map +1 -1
- package/dist/schema/diff.d.ts +103 -0
- package/dist/schema/diff.d.ts.map +1 -0
- package/dist/schema/diff.js +329 -0
- package/dist/schema/diff.js.map +1 -0
- package/dist/schema/entity-operations.d.ts +99 -0
- package/dist/schema/entity-operations.d.ts.map +1 -0
- package/dist/schema/entity-operations.js +818 -0
- package/dist/schema/entity-operations.js.map +1 -0
- package/dist/schema/index.d.ts +28 -34
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +454 -521
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/migration.d.ts +205 -0
- package/dist/schema/migration.d.ts.map +1 -0
- package/dist/schema/migration.js +327 -0
- package/dist/schema/migration.js.map +1 -0
- package/dist/schema/nl-query-generator.d.ts +68 -0
- package/dist/schema/nl-query-generator.d.ts.map +1 -0
- package/dist/schema/nl-query-generator.js +362 -0
- package/dist/schema/nl-query-generator.js.map +1 -0
- package/dist/schema/nl-query.d.ts +65 -0
- package/dist/schema/nl-query.d.ts.map +1 -0
- package/dist/schema/nl-query.js +178 -0
- package/dist/schema/nl-query.js.map +1 -0
- package/dist/schema/parse.d.ts.map +1 -1
- package/dist/schema/parse.js +144 -89
- package/dist/schema/parse.js.map +1 -1
- package/dist/schema/provider.d.ts +37 -0
- package/dist/schema/provider.d.ts.map +1 -1
- package/dist/schema/provider.js +15 -7
- package/dist/schema/provider.js.map +1 -1
- package/dist/schema/resolve.d.ts +46 -5
- package/dist/schema/resolve.d.ts.map +1 -1
- package/dist/schema/resolve.js +237 -95
- package/dist/schema/resolve.js.map +1 -1
- package/dist/schema/search-utils.d.ts +76 -0
- package/dist/schema/search-utils.d.ts.map +1 -0
- package/dist/schema/search-utils.js +86 -0
- package/dist/schema/search-utils.js.map +1 -0
- package/dist/schema/seed.d.ts +53 -0
- package/dist/schema/seed.d.ts.map +1 -0
- package/dist/schema/seed.js +94 -0
- package/dist/schema/seed.js.map +1 -0
- package/dist/schema/semantic.d.ts +10 -0
- package/dist/schema/semantic.d.ts.map +1 -1
- package/dist/schema/semantic.js +192 -86
- package/dist/schema/semantic.js.map +1 -1
- package/dist/schema/sub-apis.d.ts +52 -0
- package/dist/schema/sub-apis.d.ts.map +1 -0
- package/dist/schema/sub-apis.js +216 -0
- package/dist/schema/sub-apis.js.map +1 -0
- package/dist/schema/system-entities.d.ts +42 -0
- package/dist/schema/system-entities.d.ts.map +1 -0
- package/dist/schema/system-entities.js +101 -0
- package/dist/schema/system-entities.js.map +1 -0
- package/dist/schema/types.d.ts +91 -9
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/union-fallback.d.ts.map +1 -1
- package/dist/schema/union-fallback.js +21 -15
- package/dist/schema/union-fallback.js.map +1 -1
- package/dist/schema/value-generators/ai.d.ts +54 -0
- package/dist/schema/value-generators/ai.d.ts.map +1 -0
- package/dist/schema/value-generators/ai.js +136 -0
- package/dist/schema/value-generators/ai.js.map +1 -0
- package/dist/schema/value-generators/index.d.ts +126 -0
- package/dist/schema/value-generators/index.d.ts.map +1 -0
- package/dist/schema/value-generators/index.js +219 -0
- package/dist/schema/value-generators/index.js.map +1 -0
- package/dist/schema/value-generators/placeholder.d.ts +52 -0
- package/dist/schema/value-generators/placeholder.d.ts.map +1 -0
- package/dist/schema/value-generators/placeholder.js +328 -0
- package/dist/schema/value-generators/placeholder.js.map +1 -0
- package/dist/schema/value-generators/types.d.ts +116 -0
- package/dist/schema/value-generators/types.d.ts.map +1 -0
- package/dist/schema/value-generators/types.js +11 -0
- package/dist/schema/value-generators/types.js.map +1 -0
- package/dist/schema/version.d.ts +111 -0
- package/dist/schema/version.d.ts.map +1 -0
- package/dist/schema/version.js +190 -0
- package/dist/schema/version.js.map +1 -0
- package/dist/schema.d.ts +1095 -24
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2852 -40
- package/dist/schema.js.map +1 -1
- package/dist/semantic-vectors.d.ts +39 -0
- package/dist/semantic-vectors.d.ts.map +1 -0
- package/dist/semantic-vectors.js +334 -0
- package/dist/semantic-vectors.js.map +1 -0
- package/dist/semantic.d.ts +29 -1
- package/dist/semantic.d.ts.map +1 -1
- package/dist/semantic.js +26 -16
- package/dist/semantic.js.map +1 -1
- package/dist/telemetry.d.ts +128 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +305 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/tests.d.ts.map +1 -1
- package/dist/tests.js +30 -22
- package/dist/tests.js.map +1 -1
- package/dist/type-guards.d.ts +50 -5
- package/dist/type-guards.d.ts.map +1 -1
- package/dist/type-guards.js +87 -16
- package/dist/type-guards.js.map +1 -1
- package/dist/types.d.ts +33 -245
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +62 -72
- package/dist/types.js.map +1 -1
- package/dist/validation.d.ts +2 -5
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +65 -93
- package/dist/validation.js.map +1 -1
- package/dist/worker/db-provider.d.ts +168 -0
- package/dist/worker/db-provider.d.ts.map +1 -0
- package/dist/worker/db-provider.js +277 -0
- package/dist/worker/db-provider.js.map +1 -0
- package/dist/worker/index.d.ts +35 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +37 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker.d.ts +779 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +2786 -0
- package/dist/worker.js.map +1 -0
- package/package.json +46 -16
- package/src/docs-rels/migrations/0001-init.sql +125 -0
- package/LICENSE +0 -21
package/dist/schema/cascade.js
CHANGED
|
@@ -6,29 +6,208 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @packageDocumentation
|
|
8
8
|
*/
|
|
9
|
+
import { AIGenerationError } from '../errors.js';
|
|
9
10
|
import { isPrimitiveType } from './parse.js';
|
|
10
|
-
import { resolveNestedPending, prefetchContext, resolveInstructions } from './resolve.js';
|
|
11
|
+
import { resolveNestedPending, prefetchContext, resolveInstructions, isPromptField, } from './resolve.js';
|
|
12
|
+
import { PlaceholderValueGenerator } from './value-generators/index.js';
|
|
13
|
+
/**
|
|
14
|
+
* Hard safety limit for cascade/entity generation recursion depth.
|
|
15
|
+
* Applied even when maxDepth is not explicitly configured to prevent
|
|
16
|
+
* infinite recursion with circular schemas.
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_MAX_DEPTH = 10;
|
|
19
|
+
// Create a singleton placeholder generator for synchronous calls
|
|
20
|
+
const placeholderGenerator = new PlaceholderValueGenerator();
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Value Generator Configuration
|
|
23
|
+
// =============================================================================
|
|
24
|
+
/**
|
|
25
|
+
* Current value generator instance
|
|
26
|
+
* Defaults to PlaceholderValueGenerator
|
|
27
|
+
*/
|
|
28
|
+
let currentValueGenerator = placeholderGenerator;
|
|
29
|
+
/**
|
|
30
|
+
* Configure the value generator to use for field generation
|
|
31
|
+
*
|
|
32
|
+
* @param generator - The value generator instance to use
|
|
33
|
+
*/
|
|
34
|
+
export function setValueGenerator(generator) {
|
|
35
|
+
currentValueGenerator = generator;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get the current value generator
|
|
39
|
+
*
|
|
40
|
+
* @returns The current value generator instance
|
|
41
|
+
*/
|
|
42
|
+
export function getValueGenerator() {
|
|
43
|
+
return currentValueGenerator;
|
|
44
|
+
}
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// AI Generation Configuration
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// Default configuration - uses 'sonnet' as the default model
|
|
49
|
+
let aiConfig = {
|
|
50
|
+
model: 'sonnet',
|
|
51
|
+
enabled: true,
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Configure AI generation settings
|
|
55
|
+
*
|
|
56
|
+
* @param config - Partial configuration to merge with defaults
|
|
57
|
+
*/
|
|
58
|
+
export function configureAIGeneration(config) {
|
|
59
|
+
aiConfig = { ...aiConfig, ...config };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get current AI generation configuration
|
|
63
|
+
*/
|
|
64
|
+
export function getAIGenerationConfig() {
|
|
65
|
+
return { ...aiConfig };
|
|
66
|
+
}
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// AI-Powered Entity Generation
|
|
69
|
+
// =============================================================================
|
|
70
|
+
/**
|
|
71
|
+
* Build a simple schema object for generateObject from entity fields
|
|
72
|
+
*
|
|
73
|
+
* @param entity - The parsed entity definition
|
|
74
|
+
* @returns A simple schema object mapping field names to type descriptions
|
|
75
|
+
*/
|
|
76
|
+
function buildSchemaForEntity(entity) {
|
|
77
|
+
const schema = {};
|
|
78
|
+
for (const [fieldName, field] of entity.fields) {
|
|
79
|
+
// Only include non-relation scalar fields
|
|
80
|
+
if (!field.isRelation) {
|
|
81
|
+
const isPrompt = isPromptField(field);
|
|
82
|
+
if (field.type === 'string') {
|
|
83
|
+
// Use the field prompt if available, otherwise a generic description
|
|
84
|
+
schema[fieldName] = field.prompt || `Generate a ${fieldName}`;
|
|
85
|
+
}
|
|
86
|
+
else if (isPrompt) {
|
|
87
|
+
// For prompt fields, use the type (which is the prompt) as the schema
|
|
88
|
+
schema[fieldName] = field.type;
|
|
89
|
+
}
|
|
90
|
+
else if (field.type === 'number') {
|
|
91
|
+
schema[fieldName] = `number`;
|
|
92
|
+
}
|
|
93
|
+
else if (field.type === 'boolean') {
|
|
94
|
+
schema[fieldName] = `boolean`;
|
|
95
|
+
}
|
|
96
|
+
// Note: Arrays handled separately for now
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return schema;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Generate entity data using AI via generateObject from ai-functions
|
|
103
|
+
*
|
|
104
|
+
* @param type - The type name of the entity to generate
|
|
105
|
+
* @param entity - The parsed entity definition
|
|
106
|
+
* @param prompt - The generation prompt (from field definition)
|
|
107
|
+
* @param context - Context from parent entity for informed generation
|
|
108
|
+
* @param injectedConfig - Optional AI config to use instead of module-level config (for DI)
|
|
109
|
+
* @returns Generated entity data, or null if AI generation failed/unavailable
|
|
110
|
+
*/
|
|
111
|
+
async function generateEntityDataWithAI(type, entity, prompt, context, injectedConfig) {
|
|
112
|
+
// Use injected config if provided, otherwise fall back to module-level config
|
|
113
|
+
const config = injectedConfig ?? aiConfig;
|
|
114
|
+
if (!config.enabled) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
// Dynamically import generateObject to avoid circular dependencies
|
|
119
|
+
// and to allow the mock to work in tests
|
|
120
|
+
const { generateObject } = await import('ai-functions');
|
|
121
|
+
// Build schema for the target entity
|
|
122
|
+
const schema = buildSchemaForEntity(entity);
|
|
123
|
+
// If no fields to generate, return null
|
|
124
|
+
if (Object.keys(schema).length === 0) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
// Build comprehensive prompt with context
|
|
128
|
+
const promptParts = [];
|
|
129
|
+
// Add the field prompt (e.g., "What problem does this solve?")
|
|
130
|
+
if (prompt && prompt.trim()) {
|
|
131
|
+
promptParts.push(prompt);
|
|
132
|
+
}
|
|
133
|
+
// Add $instructions if available
|
|
134
|
+
if (context.instructions) {
|
|
135
|
+
promptParts.push(`Context: ${context.instructions}`);
|
|
136
|
+
}
|
|
137
|
+
// Add $context if available
|
|
138
|
+
if (context.schemaContext) {
|
|
139
|
+
promptParts.push(context.schemaContext);
|
|
140
|
+
}
|
|
141
|
+
// Add relevant parent data for context
|
|
142
|
+
const parentContextEntries = [];
|
|
143
|
+
for (const [key, value] of Object.entries(context.parentData)) {
|
|
144
|
+
if (!key.startsWith('$') && !key.startsWith('_') && typeof value === 'string' && value) {
|
|
145
|
+
parentContextEntries.push(`${key}: ${value}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (parentContextEntries.length > 0) {
|
|
149
|
+
promptParts.push(`Parent entity: ${parentContextEntries.join(', ')}`);
|
|
150
|
+
}
|
|
151
|
+
// Add type context
|
|
152
|
+
promptParts.push(`Generate a ${type} entity with the following fields.`);
|
|
153
|
+
const fullPrompt = promptParts.join('\n');
|
|
154
|
+
// Call generateObject with the schema and prompt, tracking timing
|
|
155
|
+
const startTime = Date.now();
|
|
156
|
+
const result = await generateObject({
|
|
157
|
+
model: config.model,
|
|
158
|
+
schema,
|
|
159
|
+
prompt: fullPrompt,
|
|
160
|
+
});
|
|
161
|
+
const latencyMs = Date.now() - startTime;
|
|
162
|
+
// Call onGenerate callback if configured
|
|
163
|
+
if (config.onGenerate) {
|
|
164
|
+
config.onGenerate({
|
|
165
|
+
entityType: type,
|
|
166
|
+
model: config.model,
|
|
167
|
+
prompt: fullPrompt,
|
|
168
|
+
result: result.object,
|
|
169
|
+
latencyMs,
|
|
170
|
+
timestamp: new Date(),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return result.object;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
// Call onGenerate callback with error if configured
|
|
177
|
+
if (config.onGenerate) {
|
|
178
|
+
config.onGenerate({
|
|
179
|
+
entityType: type,
|
|
180
|
+
model: config.model,
|
|
181
|
+
prompt: prompt || '',
|
|
182
|
+
result: null,
|
|
183
|
+
latencyMs: 0,
|
|
184
|
+
error: error instanceof Error ? error.message : String(error),
|
|
185
|
+
timestamp: new Date(),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// Throw AIGenerationError - don't silently fall back to placeholder
|
|
189
|
+
const cause = error instanceof Error ? error : undefined;
|
|
190
|
+
throw new AIGenerationError(cause?.message || 'Unknown AI generation failure', type, undefined, cause);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
11
193
|
// =============================================================================
|
|
12
194
|
// Context-Aware Value Generation
|
|
13
195
|
// =============================================================================
|
|
14
196
|
/**
|
|
15
197
|
* Generate a context-aware value for a field
|
|
16
198
|
*
|
|
17
|
-
*
|
|
18
|
-
* contextually appropriate values. This is a minimal deterministic implementation
|
|
19
|
-
* for testing - real AI generation would integrate with LLMs.
|
|
199
|
+
* **DELEGATED TO VALUE-GENERATORS MODULE**
|
|
20
200
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
201
|
+
* This function now delegates to PlaceholderValueGenerator for backward
|
|
202
|
+
* compatibility. The actual generation logic has been extracted to:
|
|
203
|
+
* `./value-generators/placeholder.ts`
|
|
24
204
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* - Allow tests to make specific assertions about generated content
|
|
205
|
+
* For new code, prefer using the ValueGenerator interface directly:
|
|
206
|
+
* ```ts
|
|
207
|
+
* import { getValueGenerator } from './value-generators/index.js'
|
|
208
|
+
* const generator = getValueGenerator()
|
|
209
|
+
* const result = await generator.generate({ fieldName, type, fullContext, hint, parentData })
|
|
210
|
+
* ```
|
|
32
211
|
*
|
|
33
212
|
* @param fieldName - The name of the field being generated
|
|
34
213
|
* @param type - The entity type name
|
|
@@ -47,180 +226,108 @@ import { resolveNestedPending, prefetchContext, resolveInstructions } from './re
|
|
|
47
226
|
* ```
|
|
48
227
|
*/
|
|
49
228
|
export function generateContextAwareValue(fieldName, type, fullContext, hint, parentData = {}) {
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (hintLower.includes('energetic') || contextLower.includes('energetic'))
|
|
74
|
-
return 'Energetic and engaging presentation style';
|
|
75
|
-
if (contextLower.includes('horror') || contextLower.includes('dark'))
|
|
76
|
-
return 'Dark and atmospheric horror style';
|
|
77
|
-
if (contextLower.includes('sci-fi') || contextLower.includes('futuristic'))
|
|
78
|
-
return 'Atmospheric sci-fi suspense style';
|
|
79
|
-
return `${fieldName}: ${fullContext}`;
|
|
80
|
-
}
|
|
81
|
-
// For 'background' field
|
|
82
|
-
if (fieldName === 'background') {
|
|
83
|
-
if (hintLower.includes('tech entrepreneur') || hintLower.includes('startup'))
|
|
84
|
-
return 'Tech startup founder with 10 years experience';
|
|
85
|
-
if (hintLower.includes('aristocrat') || hintLower.includes('noble'))
|
|
86
|
-
return 'English aristocrat from old noble family';
|
|
87
|
-
if (contextLower.includes('renewable') || contextLower.includes('energy'))
|
|
88
|
-
return 'Background in renewable energy sector';
|
|
89
|
-
return `${fieldName}: ${fullContext}`;
|
|
90
|
-
}
|
|
91
|
-
// For 'specialty' field
|
|
92
|
-
if (fieldName === 'specialty') {
|
|
93
|
-
if (contextLower.includes('french') || contextLower.includes('restaurant'))
|
|
94
|
-
return 'French classical cuisine';
|
|
95
|
-
if (hintLower.includes('security') || contextLower.includes('security'))
|
|
96
|
-
return 'Security and authentication systems';
|
|
97
|
-
if (hintLower.includes('history') || hintLower.includes('medieval'))
|
|
98
|
-
return 'Medieval history specialist';
|
|
99
|
-
return `${fieldName}: ${fullContext}`;
|
|
100
|
-
}
|
|
101
|
-
// For 'training' field
|
|
102
|
-
if (fieldName === 'training') {
|
|
103
|
-
if (contextLower.includes('french') || contextLower.includes('restaurant'))
|
|
104
|
-
return 'Trained in classical French culinary techniques';
|
|
105
|
-
return `${fieldName}: ${fullContext}`;
|
|
106
|
-
}
|
|
107
|
-
// For 'backstory' field
|
|
108
|
-
if (fieldName === 'backstory') {
|
|
109
|
-
if (contextLower.includes('medieval') || contextLower.includes('fantasy'))
|
|
110
|
-
return 'A noble knight who served the King in the great castle, completing many quests across the kingdom';
|
|
111
|
-
if (contextLower.includes('sci-fi') || contextLower.includes('space'))
|
|
112
|
-
return 'A starship captain with years of deep space exploration';
|
|
113
|
-
return `${fieldName}: ${fullContext}`;
|
|
114
|
-
}
|
|
115
|
-
// For 'headline' field
|
|
116
|
-
if (fieldName === 'headline') {
|
|
117
|
-
// Check for name mentions in context for personalized headlines
|
|
118
|
-
if (contextLower.includes('codehelper'))
|
|
119
|
-
return 'CodeHelper: Dev Tools';
|
|
120
|
-
if (contextLower.includes('techcorp'))
|
|
121
|
-
return 'TechCorp Solutions';
|
|
122
|
-
if (contextLower.includes('software engineer'))
|
|
123
|
-
return 'For Dev Teams';
|
|
124
|
-
if (contextLower.includes('tech') || contextLower.includes('startup'))
|
|
125
|
-
return 'Tech Startup Solutions';
|
|
126
|
-
return `Headline for ${type}`.slice(0, 30);
|
|
127
|
-
}
|
|
128
|
-
// For 'copy' field
|
|
129
|
-
if (fieldName === 'copy') {
|
|
130
|
-
if (contextLower.includes('tech') || contextLower.includes('startup'))
|
|
131
|
-
return 'Innovative tech solutions for startups and growing companies';
|
|
132
|
-
if (contextLower.includes('marketing') || contextLower.includes('campaign'))
|
|
133
|
-
return 'Effective marketing campaign for tech launch';
|
|
134
|
-
return `${fieldName}: ${fullContext}`;
|
|
135
|
-
}
|
|
136
|
-
// For 'tagline' field
|
|
137
|
-
if (fieldName === 'tagline') {
|
|
138
|
-
if (contextLower.includes('luxury') || contextLower.includes('premium'))
|
|
139
|
-
return 'Luxury craftsmanship meets elegant design';
|
|
140
|
-
if (contextLower.includes('quality') || contextLower.includes('craftsmanship'))
|
|
141
|
-
return 'Premium quality with expert craftsmanship';
|
|
142
|
-
if (contextLower.includes('tech'))
|
|
143
|
-
return 'Technology for the future';
|
|
144
|
-
return `${fieldName}: ${fullContext}`;
|
|
145
|
-
}
|
|
146
|
-
// For 'description' field
|
|
147
|
-
if (fieldName === 'description') {
|
|
148
|
-
if (contextLower.includes('cyberpunk') || contextLower.includes('neon') || contextLower.includes('futuristic'))
|
|
149
|
-
return 'Cyberpunk character with neural augmentations';
|
|
150
|
-
if (contextLower.includes('luxury') || contextLower.includes('high-end') || contextLower.includes('premium'))
|
|
151
|
-
return 'A luxury premium product with elegant craftsmanship';
|
|
152
|
-
if (contextLower.includes('enterprise') || contextLower.includes('b2b'))
|
|
153
|
-
return 'Enterprise solution for business customers';
|
|
154
|
-
if (contextLower.includes('nurse') || contextLower.includes('healthcare'))
|
|
155
|
-
return 'Healthcare documentation solution for nurses and medical staff';
|
|
156
|
-
return `${fieldName}: ${fullContext}`;
|
|
157
|
-
}
|
|
158
|
-
// For 'abilities' field
|
|
159
|
-
if (fieldName === 'abilities') {
|
|
160
|
-
if (contextLower.includes('cyberpunk') || contextLower.includes('futuristic'))
|
|
161
|
-
return 'Neural hacking and digital infiltration';
|
|
162
|
-
return `${fieldName}: ${fullContext}`;
|
|
163
|
-
}
|
|
164
|
-
// For 'method' field
|
|
165
|
-
if (fieldName === 'method') {
|
|
166
|
-
if (hintLower.includes('wit') || hintLower.includes('sharp'))
|
|
167
|
-
return 'Brilliant deduction and clever observation';
|
|
168
|
-
return `${fieldName}: ${fullContext}`;
|
|
169
|
-
}
|
|
170
|
-
// For 'expertise' field
|
|
171
|
-
if (fieldName === 'expertise') {
|
|
172
|
-
if (contextLower.includes('machine learning') || contextLower.includes('medical') || contextLower.includes('ai'))
|
|
173
|
-
return 'Machine learning for medical applications';
|
|
174
|
-
if (hintLower.includes('physics') || hintLower.includes('professor'))
|
|
175
|
-
return 'Physics professor specializing in quantum mechanics';
|
|
176
|
-
if (hintLower.includes('journalist') || hintLower.includes('science'))
|
|
177
|
-
return 'Science journalist covering physics research';
|
|
178
|
-
return `${fieldName}: ${fullContext}`;
|
|
179
|
-
}
|
|
180
|
-
// For 'focus' field
|
|
181
|
-
if (fieldName === 'focus') {
|
|
182
|
-
if (contextLower.includes('renewable') || contextLower.includes('energy') || contextLower.includes('green'))
|
|
183
|
-
return 'Focus on sustainable energy transformation';
|
|
184
|
-
if (contextLower.includes('tech') || contextLower.includes('programming'))
|
|
185
|
-
return 'Focus on technical programming topics';
|
|
186
|
-
return `${fieldName}: ${fullContext}`;
|
|
187
|
-
}
|
|
188
|
-
// For 'qualifications' field
|
|
189
|
-
if (fieldName === 'qualifications') {
|
|
190
|
-
if (contextLower.includes('astrophysics') || contextLower.includes('astronomy') || contextLower.includes('space'))
|
|
191
|
-
return 'PhD in Astrophysics from MIT';
|
|
192
|
-
return `${fieldName}: ${fullContext}`;
|
|
193
|
-
}
|
|
194
|
-
// For 'teachingStyle' field
|
|
195
|
-
if (fieldName === 'teachingStyle') {
|
|
196
|
-
if (contextLower.includes('beginner') || contextLower.includes('introduct'))
|
|
197
|
-
return 'Patient and accessible approach for beginners';
|
|
198
|
-
return `${fieldName}: ${fullContext}`;
|
|
199
|
-
}
|
|
200
|
-
// For 'experience' field
|
|
201
|
-
if (fieldName === 'experience') {
|
|
202
|
-
if (contextLower.includes('horror') || contextLower.includes('film'))
|
|
203
|
-
return 'Experience in horror film production';
|
|
204
|
-
return `${fieldName}: ${fullContext}`;
|
|
205
|
-
}
|
|
206
|
-
// For 'role' field
|
|
207
|
-
if (fieldName === 'role') {
|
|
208
|
-
if (hintLower.includes('research') || hintLower.includes('machine learning') || hintLower.includes('phd'))
|
|
209
|
-
return 'Machine learning researcher';
|
|
210
|
-
return `${fieldName}: ${fullContext}`;
|
|
211
|
-
}
|
|
212
|
-
// For 'portfolio' field
|
|
213
|
-
if (fieldName === 'portfolio') {
|
|
214
|
-
if (hintLower.includes('award') || hintLower.includes('beaux-arts') || hintLower.includes('ecole'))
|
|
215
|
-
return 'Award-winning design portfolio from Beaux-Arts';
|
|
216
|
-
return `${fieldName}: ${fullContext}`;
|
|
217
|
-
}
|
|
218
|
-
// Default: include context in the generated value
|
|
219
|
-
return `${fieldName}: ${fullContext}`;
|
|
229
|
+
// Use the configured value generator
|
|
230
|
+
// Note: This is synchronous, so we rely on PlaceholderValueGenerator's sync method
|
|
231
|
+
// If a custom generator is configured, it must support synchronous generation
|
|
232
|
+
const generator = currentValueGenerator;
|
|
233
|
+
// If it's a PlaceholderValueGenerator, use the sync method
|
|
234
|
+
if (generator instanceof PlaceholderValueGenerator) {
|
|
235
|
+
return generator.generateSync({
|
|
236
|
+
fieldName,
|
|
237
|
+
type,
|
|
238
|
+
fullContext,
|
|
239
|
+
parentData,
|
|
240
|
+
...(hint !== undefined && { hint }),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
// For non-PlaceholderValueGenerator instances, fall back to placeholder
|
|
244
|
+
// for backward compatibility (async generators require different call pattern)
|
|
245
|
+
return placeholderGenerator.generateSync({
|
|
246
|
+
fieldName,
|
|
247
|
+
type,
|
|
248
|
+
fullContext,
|
|
249
|
+
parentData,
|
|
250
|
+
...(hint !== undefined && { hint }),
|
|
251
|
+
});
|
|
220
252
|
}
|
|
221
253
|
// =============================================================================
|
|
222
254
|
// AI Field Generation
|
|
223
255
|
// =============================================================================
|
|
256
|
+
/**
|
|
257
|
+
* Build a combined entity object with pre-fetched context data merged in.
|
|
258
|
+
* This replaces UUID references with actual entity objects for template resolution.
|
|
259
|
+
*/
|
|
260
|
+
function buildCombinedEntityWithContext(entityData, contextData) {
|
|
261
|
+
const combined = { ...entityData };
|
|
262
|
+
// Sort context keys by length so we process parent paths before nested paths
|
|
263
|
+
// This ensures 'project' is set before 'project.lead'
|
|
264
|
+
const sortedKeys = [...contextData.keys()].sort((a, b) => a.length - b.length);
|
|
265
|
+
for (const key of sortedKeys) {
|
|
266
|
+
const value = contextData.get(key);
|
|
267
|
+
const pathParts = key.split('.');
|
|
268
|
+
if (pathParts.length === 1) {
|
|
269
|
+
// Simple path like 'project' - set at top level if currently a UUID
|
|
270
|
+
const currentValue = combined[key];
|
|
271
|
+
const isCurrentUUID = typeof currentValue === 'string' && currentValue.includes('-');
|
|
272
|
+
if (!combined[key] || isCurrentUUID) {
|
|
273
|
+
combined[key] = { ...value };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
// Nested path like 'project.lead' - traverse and set nested value
|
|
278
|
+
let current = combined;
|
|
279
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
280
|
+
const part = pathParts[i];
|
|
281
|
+
if (current[part] && typeof current[part] === 'object') {
|
|
282
|
+
current = current[part];
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const lastPart = pathParts[pathParts.length - 1];
|
|
289
|
+
if (current && typeof current === 'object') {
|
|
290
|
+
current[lastPart] = value;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Add leaf entity at top level for direct access (e.g., {lead.name})
|
|
294
|
+
if (pathParts.length > 1 && typeof value === 'object' && value !== null) {
|
|
295
|
+
const leafKey = pathParts[pathParts.length - 1];
|
|
296
|
+
if (!combined[leafKey]) {
|
|
297
|
+
combined[leafKey] = value;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return combined;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Build a context string from entity data and pre-fetched context.
|
|
305
|
+
* Extracts string fields (excluding internal fields) and formats them for AI context.
|
|
306
|
+
*/
|
|
307
|
+
function buildContextString(resolvedInstructions, entityData, contextData) {
|
|
308
|
+
const parts = [];
|
|
309
|
+
if (resolvedInstructions) {
|
|
310
|
+
parts.push(resolvedInstructions);
|
|
311
|
+
}
|
|
312
|
+
// Add entity data fields
|
|
313
|
+
for (const [key, value] of Object.entries(entityData)) {
|
|
314
|
+
if (!key.startsWith('$') && !key.startsWith('_') && typeof value === 'string' && value) {
|
|
315
|
+
parts.push(`${key}: ${value}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Add context from pre-fetched entities
|
|
319
|
+
for (const [key, ctxEntity] of contextData) {
|
|
320
|
+
for (const [fieldName, fieldValue] of Object.entries(ctxEntity)) {
|
|
321
|
+
if (!fieldName.startsWith('$') &&
|
|
322
|
+
!fieldName.startsWith('_') &&
|
|
323
|
+
typeof fieldValue === 'string' &&
|
|
324
|
+
fieldValue) {
|
|
325
|
+
parts.push(`${key}.${fieldName}: ${fieldValue}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return parts.join(' | ');
|
|
330
|
+
}
|
|
224
331
|
/**
|
|
225
332
|
* Generate AI fields based on $instructions and field prompts
|
|
226
333
|
*
|
|
@@ -232,13 +339,15 @@ export function generateContextAwareValue(fieldName, type, fullContext, hint, pa
|
|
|
232
339
|
* @param entityDef - The parsed entity definition
|
|
233
340
|
* @param schema - The parsed schema
|
|
234
341
|
* @param provider - The database provider
|
|
342
|
+
* @param injectedConfig - Optional AI config to use instead of module-level config (for DI)
|
|
235
343
|
* @returns The data with AI-generated fields populated
|
|
236
344
|
*/
|
|
237
|
-
export async function generateAIFields(data, typeName, entityDef, schema, provider) {
|
|
345
|
+
export async function generateAIFields(data, typeName, entityDef, schema, provider, injectedConfig) {
|
|
346
|
+
const config = injectedConfig ?? aiConfig;
|
|
238
347
|
const result = { ...data };
|
|
239
348
|
const entitySchema = entityDef.schema || {};
|
|
240
|
-
const instructions = entitySchema
|
|
241
|
-
const contextDeps = entitySchema
|
|
349
|
+
const instructions = entitySchema['$instructions'];
|
|
350
|
+
const contextDeps = entitySchema['$context'];
|
|
242
351
|
// Pre-fetch context dependencies if declared
|
|
243
352
|
let contextData = new Map();
|
|
244
353
|
if (contextDeps && Array.isArray(contextDeps)) {
|
|
@@ -247,54 +356,91 @@ export async function generateAIFields(data, typeName, entityDef, schema, provid
|
|
|
247
356
|
// Resolve instructions template variables
|
|
248
357
|
let resolvedInstructions = instructions;
|
|
249
358
|
if (instructions) {
|
|
250
|
-
|
|
251
|
-
const combinedEntity = { ...result };
|
|
252
|
-
for (const [key, value] of contextData) {
|
|
253
|
-
const topLevelKey = key.split('.')[0];
|
|
254
|
-
if (!combinedEntity[topLevelKey]) {
|
|
255
|
-
combinedEntity[topLevelKey] = value;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
359
|
+
const combinedEntity = buildCombinedEntityWithContext(result, contextData);
|
|
258
360
|
resolvedInstructions = await resolveInstructions(instructions, combinedEntity, typeName, schema, provider);
|
|
259
361
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
for (const [
|
|
266
|
-
|
|
267
|
-
|
|
362
|
+
const fullContext = buildContextString(resolvedInstructions, result, contextData);
|
|
363
|
+
// Collect fields that need generation
|
|
364
|
+
const fieldsToGenerate = [];
|
|
365
|
+
// When entity has $context or template variables, allow regenerating draft-generated prompt fields
|
|
366
|
+
const hasRichContext = !!(contextDeps && contextDeps.length > 0) || !!(instructions && instructions.includes('{'));
|
|
367
|
+
for (const [fieldName, field] of entityDef.fields) {
|
|
368
|
+
// Skip relation fields (handled separately)
|
|
369
|
+
if (field.isRelation) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const isPrompt = isPromptField(field);
|
|
373
|
+
// Skip if value already provided (unless AI can improve it)
|
|
374
|
+
if (result[fieldName] !== undefined && result[fieldName] !== null) {
|
|
375
|
+
// When AI is enabled, always attempt to generate prompt fields
|
|
376
|
+
// (overriding draft-generated placeholder values with AI-generated content).
|
|
377
|
+
// When AI is disabled, skip already-populated fields unless rich context is available.
|
|
378
|
+
if (isPrompt && (config.enabled || hasRichContext)) {
|
|
379
|
+
fieldsToGenerate.push({ fieldName, prompt: field.type });
|
|
380
|
+
}
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (isPrompt) {
|
|
384
|
+
// Use the field type (which is actually the prompt) as the prompt
|
|
385
|
+
fieldsToGenerate.push({ fieldName, prompt: field.type });
|
|
386
|
+
}
|
|
387
|
+
else if (field.type === 'string' && instructions) {
|
|
388
|
+
// Generate plain string fields when we have $instructions context
|
|
389
|
+
fieldsToGenerate.push({ fieldName, prompt: undefined });
|
|
268
390
|
}
|
|
269
391
|
}
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
392
|
+
// Try AI generation for all fields that need it
|
|
393
|
+
if (fieldsToGenerate.length > 0 && config.enabled) {
|
|
394
|
+
try {
|
|
395
|
+
const { generateObject } = await import('ai-functions');
|
|
396
|
+
// Build a schema for all fields to generate
|
|
397
|
+
const fieldsSchema = {};
|
|
398
|
+
for (const { fieldName, prompt } of fieldsToGenerate) {
|
|
399
|
+
fieldsSchema[fieldName] = prompt || `Generate a ${fieldName}`;
|
|
400
|
+
}
|
|
401
|
+
// Build prompt with resolved instructions
|
|
402
|
+
const promptParts = [];
|
|
403
|
+
if (resolvedInstructions) {
|
|
404
|
+
promptParts.push(resolvedInstructions);
|
|
405
|
+
}
|
|
406
|
+
promptParts.push(`Generate a ${typeName} with the following fields.`);
|
|
407
|
+
const aiPrompt = promptParts.join('\n');
|
|
408
|
+
const aiResult = await generateObject({
|
|
409
|
+
model: config.model,
|
|
410
|
+
schema: fieldsSchema,
|
|
411
|
+
prompt: aiPrompt,
|
|
412
|
+
});
|
|
413
|
+
// Apply generated values
|
|
414
|
+
const generated = aiResult.object;
|
|
415
|
+
for (const { fieldName } of fieldsToGenerate) {
|
|
416
|
+
if (generated[fieldName] !== undefined) {
|
|
417
|
+
result[fieldName] = generated[fieldName];
|
|
418
|
+
}
|
|
275
419
|
}
|
|
276
420
|
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
// Throw AIGenerationError - don't silently fall back to placeholder
|
|
423
|
+
const cause = error instanceof Error ? error : undefined;
|
|
424
|
+
throw new AIGenerationError(cause?.message || 'Unknown AI field generation failure', typeName, undefined, cause);
|
|
425
|
+
}
|
|
277
426
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
// Skip if value already provided
|
|
282
|
-
if (result[fieldName] !== undefined && result[fieldName] !== null)
|
|
283
|
-
continue;
|
|
284
|
-
// Skip relation fields (handled separately)
|
|
285
|
-
if (field.isRelation)
|
|
286
|
-
continue;
|
|
287
|
-
// Check if this is a prompt field (type contains spaces) or needs generation
|
|
288
|
-
const fieldDef = entitySchema[fieldName];
|
|
289
|
-
const isPrompt = typeof fieldDef === 'string' && fieldDef.includes(' ') && !fieldDef.includes('->');
|
|
290
|
-
if (isPrompt || (field.type === 'string' && !isPrimitiveType(field.type))) {
|
|
291
|
-
// Use the field definition as the prompt
|
|
292
|
-
const prompt = typeof fieldDef === 'string' ? fieldDef : undefined;
|
|
427
|
+
// Fill in any remaining fields with placeholder values
|
|
428
|
+
for (const { fieldName, prompt } of fieldsToGenerate) {
|
|
429
|
+
if (result[fieldName] === undefined || result[fieldName] === null) {
|
|
293
430
|
result[fieldName] = generateContextAwareValue(fieldName, typeName, fullContext, prompt, result);
|
|
294
431
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
432
|
+
}
|
|
433
|
+
// Enforce length constraints from field type hints like 'string (30 chars)'
|
|
434
|
+
for (const { fieldName, prompt } of fieldsToGenerate) {
|
|
435
|
+
if (typeof result[fieldName] === 'string' && prompt) {
|
|
436
|
+
const charMatch = prompt.match(/\((\d+)\s*chars?\)/);
|
|
437
|
+
if (charMatch) {
|
|
438
|
+
const maxLen = parseInt(charMatch[1], 10);
|
|
439
|
+
const value = result[fieldName];
|
|
440
|
+
if (value.length > maxLen) {
|
|
441
|
+
result[fieldName] = value.slice(0, maxLen);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
298
444
|
}
|
|
299
445
|
}
|
|
300
446
|
return result;
|
|
@@ -302,27 +448,87 @@ export async function generateAIFields(data, typeName, entityDef, schema, provid
|
|
|
302
448
|
// =============================================================================
|
|
303
449
|
// Entity Generation
|
|
304
450
|
// =============================================================================
|
|
451
|
+
/**
|
|
452
|
+
* Check if a field is a backward relation to a specific parent type
|
|
453
|
+
*/
|
|
454
|
+
function isBackwardRelationToParent(field, parentType) {
|
|
455
|
+
return (field.operator === '<-' && field.direction === 'backward' && field.relatedType === parentType);
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Check if a field is a required forward single relation (not array, not optional)
|
|
459
|
+
*/
|
|
460
|
+
function isRequiredForwardSingleRelation(field) {
|
|
461
|
+
return (field.operator === '->' && field.direction === 'forward' && !field.isArray && !field.isOptional);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Process relation fields and populate data with backward refs and pending forward relations
|
|
465
|
+
*
|
|
466
|
+
* This shared helper handles relations consistently whether AI generation succeeded
|
|
467
|
+
* or fell back to placeholder generation.
|
|
468
|
+
*/
|
|
469
|
+
async function processRelationFields(data, entity, type, context, schema, _depth, injectedConfig) {
|
|
470
|
+
for (const [fieldName, field] of entity.fields) {
|
|
471
|
+
if (!field.isRelation)
|
|
472
|
+
continue;
|
|
473
|
+
if (isBackwardRelationToParent(field, context.parent) && context.parentId) {
|
|
474
|
+
data[fieldName] = context.parentId;
|
|
475
|
+
}
|
|
476
|
+
else if (isRequiredForwardSingleRelation(field)) {
|
|
477
|
+
const nestedGenerated = await generateEntity(field.relatedType, field.prompt, { parent: type, parentData: data }, schema, _depth + 1, injectedConfig);
|
|
478
|
+
data[`_pending_${fieldName}`] = { type: field.relatedType, data: nestedGenerated };
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
305
482
|
/**
|
|
306
483
|
* Generate an entity based on its type and context
|
|
307
484
|
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
485
|
+
* Uses AI generation via generateObject from ai-functions when available,
|
|
486
|
+
* falling back to deterministic placeholder values for testing or when AI fails.
|
|
310
487
|
*
|
|
311
488
|
* @param type - The type of entity to generate
|
|
312
489
|
* @param prompt - Optional prompt for generation context
|
|
313
490
|
* @param context - Parent context information (parent type name, parentData, and optional parentId)
|
|
314
491
|
* @param schema - The parsed schema
|
|
492
|
+
* @param _depth - Current recursion depth (internal)
|
|
493
|
+
* @param injectedConfig - Optional AI config to use instead of module-level config (for DI)
|
|
315
494
|
*/
|
|
316
|
-
export async function generateEntity(type, prompt, context, schema) {
|
|
495
|
+
export async function generateEntity(type, prompt, context, schema, _depth = 0, injectedConfig) {
|
|
496
|
+
// Hard recursion guard to prevent infinite recursion with circular schemas
|
|
497
|
+
if (_depth >= DEFAULT_MAX_DEPTH) {
|
|
498
|
+
return {};
|
|
499
|
+
}
|
|
317
500
|
const entity = schema.entities.get(type);
|
|
318
501
|
if (!entity)
|
|
319
502
|
throw new Error(`Unknown type: ${type}`);
|
|
320
503
|
// Gather context for generation
|
|
321
504
|
const parentEntity = schema.entities.get(context.parent);
|
|
322
505
|
const parentSchema = parentEntity?.schema || {};
|
|
323
|
-
const instructions = parentSchema
|
|
324
|
-
const schemaContext = parentSchema
|
|
325
|
-
//
|
|
506
|
+
const instructions = parentSchema['$instructions'];
|
|
507
|
+
const schemaContext = parentSchema['$context'];
|
|
508
|
+
// Try AI-powered generation first
|
|
509
|
+
let aiGeneratedData = null;
|
|
510
|
+
try {
|
|
511
|
+
aiGeneratedData = await generateEntityDataWithAI(type, entity, prompt, {
|
|
512
|
+
parentData: context.parentData,
|
|
513
|
+
...(instructions !== undefined && { instructions }),
|
|
514
|
+
...(schemaContext !== undefined && { schemaContext }),
|
|
515
|
+
}, injectedConfig);
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
// If AI generation fails (e.g., ai-functions not available in tests),
|
|
519
|
+
// fall through to placeholder generation below
|
|
520
|
+
if (!(error instanceof AIGenerationError)) {
|
|
521
|
+
throw error; // Re-throw unexpected errors
|
|
522
|
+
}
|
|
523
|
+
// AIGenerationError: fall through to placeholder generation
|
|
524
|
+
}
|
|
525
|
+
// If AI generation succeeded, use that data but still handle relations
|
|
526
|
+
if (aiGeneratedData) {
|
|
527
|
+
const data = { ...aiGeneratedData };
|
|
528
|
+
await processRelationFields(data, entity, type, context, schema, _depth, injectedConfig);
|
|
529
|
+
return data;
|
|
530
|
+
}
|
|
531
|
+
// Fallback to placeholder generation if AI is not available or failed
|
|
326
532
|
const parentContextFields = [];
|
|
327
533
|
for (const [key, value] of Object.entries(context.parentData)) {
|
|
328
534
|
if (!key.startsWith('$') && !key.startsWith('_') && typeof value === 'string' && value) {
|
|
@@ -343,35 +549,21 @@ export async function generateEntity(type, prompt, context, schema) {
|
|
|
343
549
|
const data = {};
|
|
344
550
|
for (const [fieldName, field] of entity.fields) {
|
|
345
551
|
if (!field.isRelation) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
else if (field.isArray && field.type === 'string') {
|
|
351
|
-
// Generate array of strings
|
|
352
|
-
data[fieldName] = [generateContextAwareValue(fieldName, type, fullContext, prompt, context.parentData)];
|
|
552
|
+
const isPrompt = isPromptField(field);
|
|
553
|
+
if (field.type === 'string' || isPrompt) {
|
|
554
|
+
const fieldHint = isPrompt ? field.type : prompt;
|
|
555
|
+
data[fieldName] = generateContextAwareValue(fieldName, type, fullContext, fieldHint, context.parentData);
|
|
353
556
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
// Store the parent ID directly - this is a reference back to the parent
|
|
360
|
-
data[fieldName] = context.parentId;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
else if (field.operator === '->' && field.direction === 'forward') {
|
|
364
|
-
// Recursively generate nested forward exact relations
|
|
365
|
-
// This handles cases like Person.bio -> Bio
|
|
366
|
-
if (!field.isOptional) {
|
|
367
|
-
const nestedGenerated = await generateEntity(field.relatedType, field.prompt, { parent: type, parentData: data }, schema);
|
|
368
|
-
// We need to create the nested entity too, but we can't do that here
|
|
369
|
-
// because we don't have access to the provider yet.
|
|
370
|
-
// This will be handled by resolveForwardExact when it calls us
|
|
371
|
-
data[`_pending_${fieldName}`] = { type: field.relatedType, data: nestedGenerated };
|
|
557
|
+
else if (field.isArray && (field.type === 'string' || isPrompt)) {
|
|
558
|
+
const fieldHint = isPrompt ? field.type : prompt;
|
|
559
|
+
data[fieldName] = [
|
|
560
|
+
generateContextAwareValue(fieldName, type, fullContext, fieldHint, context.parentData),
|
|
561
|
+
];
|
|
372
562
|
}
|
|
373
563
|
}
|
|
374
564
|
}
|
|
565
|
+
// Process relation fields using shared helper
|
|
566
|
+
await processRelationFields(data, entity, type, context, schema, _depth, injectedConfig);
|
|
375
567
|
return data;
|
|
376
568
|
}
|
|
377
569
|
// =============================================================================
|
|
@@ -441,11 +633,11 @@ export async function resolveForwardExact(typeName, data, entity, schema, provid
|
|
|
441
633
|
// - For union types, always allow generation (we have explicit type to generate)
|
|
442
634
|
const hasUnionTypes = field.unionTypes && field.unionTypes.length > 0;
|
|
443
635
|
const shouldSkip = hasBackwardRef && hasRequiredScalarFields && !hasUnionTypes;
|
|
444
|
-
const canGenerate = !shouldSkip &&
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
636
|
+
const canGenerate = !shouldSkip &&
|
|
637
|
+
(hasBackwardRef || // Symmetric ref without required scalars
|
|
638
|
+
field.prompt || // Has a generation prompt
|
|
639
|
+
!hasRequiredScalarFields || // No required fields to worry about
|
|
640
|
+
hasUnionTypes); // Union types should generate the first type
|
|
449
641
|
if (!canGenerate)
|
|
450
642
|
continue;
|
|
451
643
|
const generated = await generateEntity(generateType, field.prompt, { parent: typeName, parentData: data, parentId }, schema);
|
|
@@ -453,23 +645,26 @@ export async function resolveForwardExact(typeName, data, entity, schema, provid
|
|
|
453
645
|
const resolvedGenerated = await resolveNestedPending(generated, relatedEntity, schema, provider);
|
|
454
646
|
const created = await provider.create(generateType, undefined, {
|
|
455
647
|
...resolvedGenerated,
|
|
456
|
-
$matchedType: generateType
|
|
648
|
+
$matchedType: generateType,
|
|
457
649
|
});
|
|
458
|
-
resolved[fieldName] = [created
|
|
650
|
+
resolved[fieldName] = [created['$id']];
|
|
459
651
|
resolved[`${fieldName}$matchedType`] = generateType;
|
|
460
652
|
// Queue relationship creation for after parent entity is created
|
|
461
|
-
pendingRelations.push({
|
|
653
|
+
pendingRelations.push({
|
|
654
|
+
fieldName,
|
|
655
|
+
targetType: generateType,
|
|
656
|
+
targetId: created['$id'],
|
|
657
|
+
});
|
|
462
658
|
}
|
|
463
659
|
else {
|
|
464
660
|
// Single non-optional forward relation - generate the related entity
|
|
465
|
-
// Generate single entity
|
|
466
661
|
const generated = await generateEntity(field.relatedType, field.prompt, { parent: typeName, parentData: data, parentId }, schema);
|
|
467
662
|
// Resolve any pending nested relations in the generated data
|
|
468
663
|
const relatedEntity = schema.entities.get(field.relatedType);
|
|
469
664
|
if (relatedEntity) {
|
|
470
665
|
const resolvedGenerated = await resolveNestedPending(generated, relatedEntity, schema, provider);
|
|
471
666
|
const created = await provider.create(field.relatedType, undefined, resolvedGenerated);
|
|
472
|
-
resolved[fieldName] = created
|
|
667
|
+
resolved[fieldName] = created['$id'];
|
|
473
668
|
}
|
|
474
669
|
}
|
|
475
670
|
}
|
|
@@ -491,10 +686,10 @@ export function generateNaturalLanguageContent(fieldName, prompt, targetType, co
|
|
|
491
686
|
// Extract key words from prompt for natural language
|
|
492
687
|
const keyWords = prompt.toLowerCase();
|
|
493
688
|
if (keyWords.includes('idea') || keyWords.includes('concept')) {
|
|
494
|
-
return `A innovative idea for ${context
|
|
689
|
+
return `A innovative idea for ${context['name'] || targetType}`;
|
|
495
690
|
}
|
|
496
691
|
if (keyWords.includes('customer') || keyWords.includes('buyer') || keyWords.includes('user')) {
|
|
497
|
-
return `The target customer segment for ${context
|
|
692
|
+
return `The target customer segment for ${context['name'] || targetType}`;
|
|
498
693
|
}
|
|
499
694
|
if (keyWords.includes('related') || keyWords.includes('similar')) {
|
|
500
695
|
return `Related ${targetType.toLowerCase()} content`;
|
|
@@ -506,37 +701,41 @@ export function generateNaturalLanguageContent(fieldName, prompt, targetType, co
|
|
|
506
701
|
// Generate based on field name patterns
|
|
507
702
|
const fieldLower = fieldName.toLowerCase();
|
|
508
703
|
if (fieldLower.includes('idea')) {
|
|
509
|
-
return `A compelling idea for ${context
|
|
704
|
+
return `A compelling idea for ${context['name'] || 'the project'}`;
|
|
510
705
|
}
|
|
511
706
|
if (fieldLower.includes('customer')) {
|
|
512
|
-
return `The ideal customer for ${context
|
|
707
|
+
return `The ideal customer for ${context['name'] || 'the business'}`;
|
|
513
708
|
}
|
|
514
|
-
if (fieldLower.includes('founder') ||
|
|
709
|
+
if (fieldLower.includes('founder') ||
|
|
710
|
+
fieldLower.includes('lead') ||
|
|
711
|
+
fieldLower.includes('ceo') ||
|
|
712
|
+
fieldLower.includes('cto') ||
|
|
713
|
+
fieldLower.includes('cfo')) {
|
|
515
714
|
return `A qualified ${fieldName} candidate`;
|
|
516
715
|
}
|
|
517
716
|
if (fieldLower.includes('author') || fieldLower.includes('reviewer')) {
|
|
518
717
|
return `An experienced ${fieldName}`;
|
|
519
718
|
}
|
|
520
719
|
if (fieldLower.includes('assignee') || fieldLower.includes('owner')) {
|
|
521
|
-
return `The right person for ${context
|
|
720
|
+
return `The right person for ${context['title'] || context['name'] || 'this task'}`;
|
|
522
721
|
}
|
|
523
722
|
if (fieldLower.includes('department') || fieldLower.includes('team')) {
|
|
524
|
-
return `A department for ${context
|
|
723
|
+
return `A department for ${context['name'] || 'the organization'}`;
|
|
525
724
|
}
|
|
526
725
|
if (fieldLower.includes('client') || fieldLower.includes('sponsor')) {
|
|
527
|
-
return `A ${fieldName} for ${context
|
|
726
|
+
return `A ${fieldName} for ${context['name'] || context['title'] || 'the project'}`;
|
|
528
727
|
}
|
|
529
728
|
if (fieldLower.includes('item') || fieldLower.includes('component')) {
|
|
530
729
|
return `${targetType} component`;
|
|
531
730
|
}
|
|
532
731
|
if (fieldLower.includes('member') || fieldLower.includes('project')) {
|
|
533
|
-
return `${targetType} for ${context
|
|
732
|
+
return `${targetType} for ${context['name'] || 'the team'}`;
|
|
534
733
|
}
|
|
535
734
|
if (fieldLower.includes('character')) {
|
|
536
|
-
return `A character for ${context
|
|
735
|
+
return `A character for ${context['title'] || context['name'] || 'the story'}`;
|
|
537
736
|
}
|
|
538
737
|
if (fieldLower.includes('setting') || fieldLower.includes('location')) {
|
|
539
|
-
return `A setting for ${context
|
|
738
|
+
return `A setting for ${context['title'] || context['name'] || 'the story'}`;
|
|
540
739
|
}
|
|
541
740
|
if (fieldLower.includes('address')) {
|
|
542
741
|
return `Address information`;
|