ai-database 2.1.1 → 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 +47 -1
- package/README.md +1063 -186
- 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 +52 -23
- package/dist/ai-promise-db.d.ts.map +1 -1
- package/dist/ai-promise-db.js +185 -164
- 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 +37 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +112 -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 +129 -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 +49 -10
- package/dist/schema/cascade.d.ts.map +1 -1
- package/dist/schema/cascade.js +491 -273
- 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 +45 -0
- package/dist/schema/dependency-graph.d.ts.map +1 -0
- package/dist/schema/dependency-graph.js +47 -0
- package/dist/schema/dependency-graph.js.map +1 -0
- 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/generation-context.d.ts +202 -0
- package/dist/schema/generation-context.d.ts.map +1 -0
- package/dist/schema/generation-context.js +393 -0
- package/dist/schema/generation-context.js.map +1 -0
- package/dist/schema/index.d.ts +32 -34
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +462 -519
- 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 +152 -89
- package/dist/schema/parse.js.map +1 -1
- package/dist/schema/provider.d.ts +38 -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 +334 -117
- 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 +11 -0
- package/dist/schema/semantic.d.ts.map +1 -1
- package/dist/schema/semantic.js +262 -68
- 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 +219 -0
- package/dist/schema/union-fallback.d.ts.map +1 -0
- package/dist/schema/union-fallback.js +331 -0
- package/dist/schema/union-fallback.js.map +1 -0
- 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/verb-derivation.d.ts +167 -0
- package/dist/schema/verb-derivation.d.ts.map +1 -0
- package/dist/schema/verb-derivation.js +281 -0
- package/dist/schema/verb-derivation.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 -23
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2854 -38
- 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 +212 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +318 -0
- package/dist/type-guards.js.map +1 -0
- 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 +165 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +639 -0
- package/dist/validation.js.map +1 -0
- 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 +38 -8
- package/src/docs-rels/migrations/0001-init.sql +125 -0
package/dist/schema/cascade.js
CHANGED
|
@@ -6,21 +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`
|
|
204
|
+
*
|
|
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
|
+
* ```
|
|
24
211
|
*
|
|
25
212
|
* @param fieldName - The name of the field being generated
|
|
26
213
|
* @param type - The entity type name
|
|
@@ -39,180 +226,108 @@ import { resolveNestedPending, prefetchContext, resolveInstructions } from './re
|
|
|
39
226
|
* ```
|
|
40
227
|
*/
|
|
41
228
|
export function generateContextAwareValue(fieldName, type, fullContext, hint, parentData = {}) {
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (hintLower.includes('energetic') || contextLower.includes('energetic'))
|
|
66
|
-
return 'Energetic and engaging presentation style';
|
|
67
|
-
if (contextLower.includes('horror') || contextLower.includes('dark'))
|
|
68
|
-
return 'Dark and atmospheric horror style';
|
|
69
|
-
if (contextLower.includes('sci-fi') || contextLower.includes('futuristic'))
|
|
70
|
-
return 'Atmospheric sci-fi suspense style';
|
|
71
|
-
return `${fieldName}: ${fullContext}`;
|
|
72
|
-
}
|
|
73
|
-
// For 'background' field
|
|
74
|
-
if (fieldName === 'background') {
|
|
75
|
-
if (hintLower.includes('tech entrepreneur') || hintLower.includes('startup'))
|
|
76
|
-
return 'Tech startup founder with 10 years experience';
|
|
77
|
-
if (hintLower.includes('aristocrat') || hintLower.includes('noble'))
|
|
78
|
-
return 'English aristocrat from old noble family';
|
|
79
|
-
if (contextLower.includes('renewable') || contextLower.includes('energy'))
|
|
80
|
-
return 'Background in renewable energy sector';
|
|
81
|
-
return `${fieldName}: ${fullContext}`;
|
|
82
|
-
}
|
|
83
|
-
// For 'specialty' field
|
|
84
|
-
if (fieldName === 'specialty') {
|
|
85
|
-
if (contextLower.includes('french') || contextLower.includes('restaurant'))
|
|
86
|
-
return 'French classical cuisine';
|
|
87
|
-
if (hintLower.includes('security') || contextLower.includes('security'))
|
|
88
|
-
return 'Security and authentication systems';
|
|
89
|
-
if (hintLower.includes('history') || hintLower.includes('medieval'))
|
|
90
|
-
return 'Medieval history specialist';
|
|
91
|
-
return `${fieldName}: ${fullContext}`;
|
|
92
|
-
}
|
|
93
|
-
// For 'training' field
|
|
94
|
-
if (fieldName === 'training') {
|
|
95
|
-
if (contextLower.includes('french') || contextLower.includes('restaurant'))
|
|
96
|
-
return 'Trained in classical French culinary techniques';
|
|
97
|
-
return `${fieldName}: ${fullContext}`;
|
|
98
|
-
}
|
|
99
|
-
// For 'backstory' field
|
|
100
|
-
if (fieldName === 'backstory') {
|
|
101
|
-
if (contextLower.includes('medieval') || contextLower.includes('fantasy'))
|
|
102
|
-
return 'A noble knight who served the King in the great castle, completing many quests across the kingdom';
|
|
103
|
-
if (contextLower.includes('sci-fi') || contextLower.includes('space'))
|
|
104
|
-
return 'A starship captain with years of deep space exploration';
|
|
105
|
-
return `${fieldName}: ${fullContext}`;
|
|
106
|
-
}
|
|
107
|
-
// For 'headline' field
|
|
108
|
-
if (fieldName === 'headline') {
|
|
109
|
-
// Check for name mentions in context for personalized headlines
|
|
110
|
-
if (contextLower.includes('codehelper'))
|
|
111
|
-
return 'CodeHelper: Dev Tools';
|
|
112
|
-
if (contextLower.includes('techcorp'))
|
|
113
|
-
return 'TechCorp Solutions';
|
|
114
|
-
if (contextLower.includes('software engineer'))
|
|
115
|
-
return 'For Dev Teams';
|
|
116
|
-
if (contextLower.includes('tech') || contextLower.includes('startup'))
|
|
117
|
-
return 'Tech Startup Solutions';
|
|
118
|
-
return `Headline for ${type}`.slice(0, 30);
|
|
119
|
-
}
|
|
120
|
-
// For 'copy' field
|
|
121
|
-
if (fieldName === 'copy') {
|
|
122
|
-
if (contextLower.includes('tech') || contextLower.includes('startup'))
|
|
123
|
-
return 'Innovative tech solutions for startups and growing companies';
|
|
124
|
-
if (contextLower.includes('marketing') || contextLower.includes('campaign'))
|
|
125
|
-
return 'Effective marketing campaign for tech launch';
|
|
126
|
-
return `${fieldName}: ${fullContext}`;
|
|
127
|
-
}
|
|
128
|
-
// For 'tagline' field
|
|
129
|
-
if (fieldName === 'tagline') {
|
|
130
|
-
if (contextLower.includes('luxury') || contextLower.includes('premium'))
|
|
131
|
-
return 'Luxury craftsmanship meets elegant design';
|
|
132
|
-
if (contextLower.includes('quality') || contextLower.includes('craftsmanship'))
|
|
133
|
-
return 'Premium quality with expert craftsmanship';
|
|
134
|
-
if (contextLower.includes('tech'))
|
|
135
|
-
return 'Technology for the future';
|
|
136
|
-
return `${fieldName}: ${fullContext}`;
|
|
137
|
-
}
|
|
138
|
-
// For 'description' field
|
|
139
|
-
if (fieldName === 'description') {
|
|
140
|
-
if (contextLower.includes('cyberpunk') || contextLower.includes('neon') || contextLower.includes('futuristic'))
|
|
141
|
-
return 'Cyberpunk character with neural augmentations';
|
|
142
|
-
if (contextLower.includes('luxury') || contextLower.includes('high-end') || contextLower.includes('premium'))
|
|
143
|
-
return 'A luxury premium product with elegant craftsmanship';
|
|
144
|
-
if (contextLower.includes('enterprise') || contextLower.includes('b2b'))
|
|
145
|
-
return 'Enterprise solution for business customers';
|
|
146
|
-
if (contextLower.includes('nurse') || contextLower.includes('healthcare'))
|
|
147
|
-
return 'Healthcare documentation solution for nurses and medical staff';
|
|
148
|
-
return `${fieldName}: ${fullContext}`;
|
|
149
|
-
}
|
|
150
|
-
// For 'abilities' field
|
|
151
|
-
if (fieldName === 'abilities') {
|
|
152
|
-
if (contextLower.includes('cyberpunk') || contextLower.includes('futuristic'))
|
|
153
|
-
return 'Neural hacking and digital infiltration';
|
|
154
|
-
return `${fieldName}: ${fullContext}`;
|
|
155
|
-
}
|
|
156
|
-
// For 'method' field
|
|
157
|
-
if (fieldName === 'method') {
|
|
158
|
-
if (hintLower.includes('wit') || hintLower.includes('sharp'))
|
|
159
|
-
return 'Brilliant deduction and clever observation';
|
|
160
|
-
return `${fieldName}: ${fullContext}`;
|
|
161
|
-
}
|
|
162
|
-
// For 'expertise' field
|
|
163
|
-
if (fieldName === 'expertise') {
|
|
164
|
-
if (contextLower.includes('machine learning') || contextLower.includes('medical') || contextLower.includes('ai'))
|
|
165
|
-
return 'Machine learning for medical applications';
|
|
166
|
-
if (hintLower.includes('physics') || hintLower.includes('professor'))
|
|
167
|
-
return 'Physics professor specializing in quantum mechanics';
|
|
168
|
-
if (hintLower.includes('journalist') || hintLower.includes('science'))
|
|
169
|
-
return 'Science journalist covering physics research';
|
|
170
|
-
return `${fieldName}: ${fullContext}`;
|
|
171
|
-
}
|
|
172
|
-
// For 'focus' field
|
|
173
|
-
if (fieldName === 'focus') {
|
|
174
|
-
if (contextLower.includes('renewable') || contextLower.includes('energy') || contextLower.includes('green'))
|
|
175
|
-
return 'Focus on sustainable energy transformation';
|
|
176
|
-
if (contextLower.includes('tech') || contextLower.includes('programming'))
|
|
177
|
-
return 'Focus on technical programming topics';
|
|
178
|
-
return `${fieldName}: ${fullContext}`;
|
|
179
|
-
}
|
|
180
|
-
// For 'qualifications' field
|
|
181
|
-
if (fieldName === 'qualifications') {
|
|
182
|
-
if (contextLower.includes('astrophysics') || contextLower.includes('astronomy') || contextLower.includes('space'))
|
|
183
|
-
return 'PhD in Astrophysics from MIT';
|
|
184
|
-
return `${fieldName}: ${fullContext}`;
|
|
185
|
-
}
|
|
186
|
-
// For 'teachingStyle' field
|
|
187
|
-
if (fieldName === 'teachingStyle') {
|
|
188
|
-
if (contextLower.includes('beginner') || contextLower.includes('introduct'))
|
|
189
|
-
return 'Patient and accessible approach for beginners';
|
|
190
|
-
return `${fieldName}: ${fullContext}`;
|
|
191
|
-
}
|
|
192
|
-
// For 'experience' field
|
|
193
|
-
if (fieldName === 'experience') {
|
|
194
|
-
if (contextLower.includes('horror') || contextLower.includes('film'))
|
|
195
|
-
return 'Experience in horror film production';
|
|
196
|
-
return `${fieldName}: ${fullContext}`;
|
|
197
|
-
}
|
|
198
|
-
// For 'role' field
|
|
199
|
-
if (fieldName === 'role') {
|
|
200
|
-
if (hintLower.includes('research') || hintLower.includes('machine learning') || hintLower.includes('phd'))
|
|
201
|
-
return 'Machine learning researcher';
|
|
202
|
-
return `${fieldName}: ${fullContext}`;
|
|
203
|
-
}
|
|
204
|
-
// For 'portfolio' field
|
|
205
|
-
if (fieldName === 'portfolio') {
|
|
206
|
-
if (hintLower.includes('award') || hintLower.includes('beaux-arts') || hintLower.includes('ecole'))
|
|
207
|
-
return 'Award-winning design portfolio from Beaux-Arts';
|
|
208
|
-
return `${fieldName}: ${fullContext}`;
|
|
209
|
-
}
|
|
210
|
-
// Default: include context in the generated value
|
|
211
|
-
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
|
+
});
|
|
212
252
|
}
|
|
213
253
|
// =============================================================================
|
|
214
254
|
// AI Field Generation
|
|
215
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
|
+
}
|
|
216
331
|
/**
|
|
217
332
|
* Generate AI fields based on $instructions and field prompts
|
|
218
333
|
*
|
|
@@ -224,13 +339,15 @@ export function generateContextAwareValue(fieldName, type, fullContext, hint, pa
|
|
|
224
339
|
* @param entityDef - The parsed entity definition
|
|
225
340
|
* @param schema - The parsed schema
|
|
226
341
|
* @param provider - The database provider
|
|
342
|
+
* @param injectedConfig - Optional AI config to use instead of module-level config (for DI)
|
|
227
343
|
* @returns The data with AI-generated fields populated
|
|
228
344
|
*/
|
|
229
|
-
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;
|
|
230
347
|
const result = { ...data };
|
|
231
348
|
const entitySchema = entityDef.schema || {};
|
|
232
|
-
const instructions = entitySchema
|
|
233
|
-
const contextDeps = entitySchema
|
|
349
|
+
const instructions = entitySchema['$instructions'];
|
|
350
|
+
const contextDeps = entitySchema['$context'];
|
|
234
351
|
// Pre-fetch context dependencies if declared
|
|
235
352
|
let contextData = new Map();
|
|
236
353
|
if (contextDeps && Array.isArray(contextDeps)) {
|
|
@@ -239,54 +356,91 @@ export async function generateAIFields(data, typeName, entityDef, schema, provid
|
|
|
239
356
|
// Resolve instructions template variables
|
|
240
357
|
let resolvedInstructions = instructions;
|
|
241
358
|
if (instructions) {
|
|
242
|
-
|
|
243
|
-
const combinedEntity = { ...result };
|
|
244
|
-
for (const [key, value] of contextData) {
|
|
245
|
-
const topLevelKey = key.split('.')[0];
|
|
246
|
-
if (!combinedEntity[topLevelKey]) {
|
|
247
|
-
combinedEntity[topLevelKey] = value;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
359
|
+
const combinedEntity = buildCombinedEntityWithContext(result, contextData);
|
|
250
360
|
resolvedInstructions = await resolveInstructions(instructions, combinedEntity, typeName, schema, provider);
|
|
251
361
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
for (const [
|
|
258
|
-
|
|
259
|
-
|
|
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 });
|
|
260
390
|
}
|
|
261
391
|
}
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
+
}
|
|
267
419
|
}
|
|
268
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
|
+
}
|
|
269
426
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// Skip if value already provided
|
|
274
|
-
if (result[fieldName] !== undefined && result[fieldName] !== null)
|
|
275
|
-
continue;
|
|
276
|
-
// Skip relation fields (handled separately)
|
|
277
|
-
if (field.isRelation)
|
|
278
|
-
continue;
|
|
279
|
-
// Check if this is a prompt field (type contains spaces) or needs generation
|
|
280
|
-
const fieldDef = entitySchema[fieldName];
|
|
281
|
-
const isPrompt = typeof fieldDef === 'string' && fieldDef.includes(' ') && !fieldDef.includes('->');
|
|
282
|
-
if (isPrompt || (field.type === 'string' && !isPrimitiveType(field.type))) {
|
|
283
|
-
// Use the field definition as the prompt
|
|
284
|
-
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) {
|
|
285
430
|
result[fieldName] = generateContextAwareValue(fieldName, typeName, fullContext, prompt, result);
|
|
286
431
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
+
}
|
|
290
444
|
}
|
|
291
445
|
}
|
|
292
446
|
return result;
|
|
@@ -294,27 +448,87 @@ export async function generateAIFields(data, typeName, entityDef, schema, provid
|
|
|
294
448
|
// =============================================================================
|
|
295
449
|
// Entity Generation
|
|
296
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
|
+
}
|
|
297
482
|
/**
|
|
298
483
|
* Generate an entity based on its type and context
|
|
299
484
|
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
485
|
+
* Uses AI generation via generateObject from ai-functions when available,
|
|
486
|
+
* falling back to deterministic placeholder values for testing or when AI fails.
|
|
302
487
|
*
|
|
303
488
|
* @param type - The type of entity to generate
|
|
304
489
|
* @param prompt - Optional prompt for generation context
|
|
305
490
|
* @param context - Parent context information (parent type name, parentData, and optional parentId)
|
|
306
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)
|
|
307
494
|
*/
|
|
308
|
-
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
|
+
}
|
|
309
500
|
const entity = schema.entities.get(type);
|
|
310
501
|
if (!entity)
|
|
311
502
|
throw new Error(`Unknown type: ${type}`);
|
|
312
503
|
// Gather context for generation
|
|
313
504
|
const parentEntity = schema.entities.get(context.parent);
|
|
314
505
|
const parentSchema = parentEntity?.schema || {};
|
|
315
|
-
const instructions = parentSchema
|
|
316
|
-
const schemaContext = parentSchema
|
|
317
|
-
//
|
|
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
|
|
318
532
|
const parentContextFields = [];
|
|
319
533
|
for (const [key, value] of Object.entries(context.parentData)) {
|
|
320
534
|
if (!key.startsWith('$') && !key.startsWith('_') && typeof value === 'string' && value) {
|
|
@@ -335,35 +549,21 @@ export async function generateEntity(type, prompt, context, schema) {
|
|
|
335
549
|
const data = {};
|
|
336
550
|
for (const [fieldName, field] of entity.fields) {
|
|
337
551
|
if (!field.isRelation) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
else if (field.isArray && field.type === 'string') {
|
|
343
|
-
// Generate array of strings
|
|
344
|
-
data[fieldName] = [generateContextAwareValue(fieldName, type, fullContext, prompt, context.parentData)];
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
else if (field.operator === '<-' && field.direction === 'backward') {
|
|
348
|
-
// Backward relation to parent - set the parent's ID if this entity's
|
|
349
|
-
// related type matches the parent type
|
|
350
|
-
if (field.relatedType === context.parent && context.parentId) {
|
|
351
|
-
// Store the parent ID directly - this is a reference back to the parent
|
|
352
|
-
data[fieldName] = context.parentId;
|
|
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
|
-
const nestedGenerated = await generateEntity(field.relatedType, field.prompt, { parent: type, parentData: data }, schema);
|
|
360
|
-
// We need to create the nested entity too, but we can't do that here
|
|
361
|
-
// because we don't have access to the provider yet.
|
|
362
|
-
// This will be handled by resolveForwardExact when it calls us
|
|
363
|
-
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
|
+
];
|
|
364
562
|
}
|
|
365
563
|
}
|
|
366
564
|
}
|
|
565
|
+
// Process relation fields using shared helper
|
|
566
|
+
await processRelationFields(data, entity, type, context, schema, _depth, injectedConfig);
|
|
367
567
|
return data;
|
|
368
568
|
}
|
|
369
569
|
// =============================================================================
|
|
@@ -402,7 +602,11 @@ export async function resolveForwardExact(typeName, data, entity, schema, provid
|
|
|
402
602
|
continue;
|
|
403
603
|
if (field.isArray) {
|
|
404
604
|
// Forward array relation - check if we should auto-generate
|
|
405
|
-
|
|
605
|
+
// For union types, use the first union type as the generation target
|
|
606
|
+
const generateType = field.unionTypes && field.unionTypes.length > 0
|
|
607
|
+
? field.unionTypes[0]
|
|
608
|
+
: field.relatedType;
|
|
609
|
+
const relatedEntity = schema.entities.get(generateType);
|
|
406
610
|
if (!relatedEntity)
|
|
407
611
|
continue;
|
|
408
612
|
// Check if related entity has a backward ref to this type (symmetric relationship)
|
|
@@ -426,31 +630,41 @@ export async function resolveForwardExact(typeName, data, entity, schema, provid
|
|
|
426
630
|
// Decide whether to auto-generate:
|
|
427
631
|
// - If there's a symmetric backward ref AND required scalars, skip (prevents duplicates)
|
|
428
632
|
// - Otherwise, generate if the related entity can be meaningfully generated
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
633
|
+
// - For union types, always allow generation (we have explicit type to generate)
|
|
634
|
+
const hasUnionTypes = field.unionTypes && field.unionTypes.length > 0;
|
|
635
|
+
const shouldSkip = hasBackwardRef && hasRequiredScalarFields && !hasUnionTypes;
|
|
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
|
|
434
641
|
if (!canGenerate)
|
|
435
642
|
continue;
|
|
436
|
-
const generated = await generateEntity(
|
|
643
|
+
const generated = await generateEntity(generateType, field.prompt, { parent: typeName, parentData: data, parentId }, schema);
|
|
437
644
|
// Resolve any pending nested relations in the generated data
|
|
438
645
|
const resolvedGenerated = await resolveNestedPending(generated, relatedEntity, schema, provider);
|
|
439
|
-
const created = await provider.create(
|
|
440
|
-
|
|
646
|
+
const created = await provider.create(generateType, undefined, {
|
|
647
|
+
...resolvedGenerated,
|
|
648
|
+
$matchedType: generateType,
|
|
649
|
+
});
|
|
650
|
+
resolved[fieldName] = [created['$id']];
|
|
651
|
+
resolved[`${fieldName}$matchedType`] = generateType;
|
|
441
652
|
// Queue relationship creation for after parent entity is created
|
|
442
|
-
pendingRelations.push({
|
|
653
|
+
pendingRelations.push({
|
|
654
|
+
fieldName,
|
|
655
|
+
targetType: generateType,
|
|
656
|
+
targetId: created['$id'],
|
|
657
|
+
});
|
|
443
658
|
}
|
|
444
659
|
else {
|
|
445
660
|
// Single non-optional forward relation - generate the related entity
|
|
446
|
-
// Generate single entity
|
|
447
661
|
const generated = await generateEntity(field.relatedType, field.prompt, { parent: typeName, parentData: data, parentId }, schema);
|
|
448
662
|
// Resolve any pending nested relations in the generated data
|
|
449
663
|
const relatedEntity = schema.entities.get(field.relatedType);
|
|
450
664
|
if (relatedEntity) {
|
|
451
665
|
const resolvedGenerated = await resolveNestedPending(generated, relatedEntity, schema, provider);
|
|
452
666
|
const created = await provider.create(field.relatedType, undefined, resolvedGenerated);
|
|
453
|
-
resolved[fieldName] = created
|
|
667
|
+
resolved[fieldName] = created['$id'];
|
|
454
668
|
}
|
|
455
669
|
}
|
|
456
670
|
}
|
|
@@ -472,10 +686,10 @@ export function generateNaturalLanguageContent(fieldName, prompt, targetType, co
|
|
|
472
686
|
// Extract key words from prompt for natural language
|
|
473
687
|
const keyWords = prompt.toLowerCase();
|
|
474
688
|
if (keyWords.includes('idea') || keyWords.includes('concept')) {
|
|
475
|
-
return `A innovative idea for ${context
|
|
689
|
+
return `A innovative idea for ${context['name'] || targetType}`;
|
|
476
690
|
}
|
|
477
691
|
if (keyWords.includes('customer') || keyWords.includes('buyer') || keyWords.includes('user')) {
|
|
478
|
-
return `The target customer segment for ${context
|
|
692
|
+
return `The target customer segment for ${context['name'] || targetType}`;
|
|
479
693
|
}
|
|
480
694
|
if (keyWords.includes('related') || keyWords.includes('similar')) {
|
|
481
695
|
return `Related ${targetType.toLowerCase()} content`;
|
|
@@ -487,37 +701,41 @@ export function generateNaturalLanguageContent(fieldName, prompt, targetType, co
|
|
|
487
701
|
// Generate based on field name patterns
|
|
488
702
|
const fieldLower = fieldName.toLowerCase();
|
|
489
703
|
if (fieldLower.includes('idea')) {
|
|
490
|
-
return `A compelling idea for ${context
|
|
704
|
+
return `A compelling idea for ${context['name'] || 'the project'}`;
|
|
491
705
|
}
|
|
492
706
|
if (fieldLower.includes('customer')) {
|
|
493
|
-
return `The ideal customer for ${context
|
|
707
|
+
return `The ideal customer for ${context['name'] || 'the business'}`;
|
|
494
708
|
}
|
|
495
|
-
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')) {
|
|
496
714
|
return `A qualified ${fieldName} candidate`;
|
|
497
715
|
}
|
|
498
716
|
if (fieldLower.includes('author') || fieldLower.includes('reviewer')) {
|
|
499
717
|
return `An experienced ${fieldName}`;
|
|
500
718
|
}
|
|
501
719
|
if (fieldLower.includes('assignee') || fieldLower.includes('owner')) {
|
|
502
|
-
return `The right person for ${context
|
|
720
|
+
return `The right person for ${context['title'] || context['name'] || 'this task'}`;
|
|
503
721
|
}
|
|
504
722
|
if (fieldLower.includes('department') || fieldLower.includes('team')) {
|
|
505
|
-
return `A department for ${context
|
|
723
|
+
return `A department for ${context['name'] || 'the organization'}`;
|
|
506
724
|
}
|
|
507
725
|
if (fieldLower.includes('client') || fieldLower.includes('sponsor')) {
|
|
508
|
-
return `A ${fieldName} for ${context
|
|
726
|
+
return `A ${fieldName} for ${context['name'] || context['title'] || 'the project'}`;
|
|
509
727
|
}
|
|
510
728
|
if (fieldLower.includes('item') || fieldLower.includes('component')) {
|
|
511
729
|
return `${targetType} component`;
|
|
512
730
|
}
|
|
513
731
|
if (fieldLower.includes('member') || fieldLower.includes('project')) {
|
|
514
|
-
return `${targetType} for ${context
|
|
732
|
+
return `${targetType} for ${context['name'] || 'the team'}`;
|
|
515
733
|
}
|
|
516
734
|
if (fieldLower.includes('character')) {
|
|
517
|
-
return `A character for ${context
|
|
735
|
+
return `A character for ${context['title'] || context['name'] || 'the story'}`;
|
|
518
736
|
}
|
|
519
737
|
if (fieldLower.includes('setting') || fieldLower.includes('location')) {
|
|
520
|
-
return `A setting for ${context
|
|
738
|
+
return `A setting for ${context['title'] || context['name'] || 'the story'}`;
|
|
521
739
|
}
|
|
522
740
|
if (fieldLower.includes('address')) {
|
|
523
741
|
return `Address information`;
|