@objectql/core 1.7.2 → 1.7.3

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.
@@ -0,0 +1,888 @@
1
+ /**
2
+ * ObjectQL AI Agent - Programmatic API for AI-powered application generation
3
+ *
4
+ * This module provides a high-level API for using AI to generate and validate
5
+ * ObjectQL metadata programmatically.
6
+ */
7
+
8
+ import OpenAI from 'openai';
9
+ import * as yaml from 'js-yaml';
10
+ import { Validator } from './validator';
11
+ import {
12
+ ObjectConfig,
13
+ AnyValidationRule,
14
+ ValidationContext,
15
+ ValidationResult
16
+ } from '@objectql/types';
17
+
18
+ /**
19
+ * Configuration for the ObjectQL AI Agent
20
+ */
21
+ export interface AgentConfig {
22
+ /** OpenAI API key */
23
+ apiKey: string;
24
+ /** OpenAI model to use (default: gpt-4) */
25
+ model?: string;
26
+ /** Temperature for generation (0-1, default: 0.7) */
27
+ temperature?: number;
28
+ /** Preferred language for messages (default: en) */
29
+ language?: string;
30
+ }
31
+
32
+ /**
33
+ * Options for generating application metadata
34
+ */
35
+ export interface GenerateAppOptions {
36
+ /** Natural language description of the application */
37
+ description: string;
38
+ /** Type of generation: basic (minimal), complete (comprehensive), or custom */
39
+ type?: 'basic' | 'complete' | 'custom';
40
+ /** Maximum tokens for generation */
41
+ maxTokens?: number;
42
+ }
43
+
44
+ /**
45
+ * Result of application generation
46
+ */
47
+ export interface GenerateAppResult {
48
+ /** Whether generation was successful */
49
+ success: boolean;
50
+ /** Generated metadata files */
51
+ files: Array<{
52
+ filename: string;
53
+ content: string;
54
+ type: 'object' | 'validation' | 'form' | 'view' | 'page' | 'menu' | 'action' | 'hook' | 'permission' | 'workflow' | 'report' | 'data' | 'application' | 'typescript' | 'test' | 'other';
55
+ }>;
56
+ /** Any errors encountered */
57
+ errors?: string[];
58
+ /** AI model response (raw) */
59
+ rawResponse?: string;
60
+ }
61
+
62
+ /**
63
+ * Conversation message for step-by-step generation
64
+ */
65
+ export interface ConversationMessage {
66
+ role: 'system' | 'user' | 'assistant';
67
+ content: string;
68
+ }
69
+
70
+ /**
71
+ * Options for conversational generation
72
+ */
73
+ export interface ConversationalGenerateOptions {
74
+ /** Initial description or follow-up request */
75
+ message: string;
76
+ /** Previous conversation history */
77
+ conversationHistory?: ConversationMessage[];
78
+ /** Current application state (already generated files) */
79
+ currentApp?: GenerateAppResult;
80
+ }
81
+
82
+ /**
83
+ * Result of conversational generation
84
+ */
85
+ export interface ConversationalGenerateResult extends GenerateAppResult {
86
+ /** Updated conversation history */
87
+ conversationHistory: ConversationMessage[];
88
+ /** Suggested next steps or questions */
89
+ suggestions?: string[];
90
+ }
91
+
92
+ /**
93
+ * Options for validating metadata
94
+ */
95
+ export interface ValidateMetadataOptions {
96
+ /** Metadata content (YAML string or parsed object) */
97
+ metadata: string | any;
98
+ /** Filename (for context) */
99
+ filename?: string;
100
+ /** Whether to check business logic consistency */
101
+ checkBusinessLogic?: boolean;
102
+ /** Whether to check performance considerations */
103
+ checkPerformance?: boolean;
104
+ /** Whether to check security issues */
105
+ checkSecurity?: boolean;
106
+ }
107
+
108
+ /**
109
+ * Result of metadata validation
110
+ */
111
+ export interface ValidateMetadataResult {
112
+ /** Whether validation passed (no errors) */
113
+ valid: boolean;
114
+ /** Errors found */
115
+ errors: Array<{
116
+ message: string;
117
+ location?: string;
118
+ code?: string;
119
+ }>;
120
+ /** Warnings found */
121
+ warnings: Array<{
122
+ message: string;
123
+ location?: string;
124
+ suggestion?: string;
125
+ }>;
126
+ /** Informational messages */
127
+ info: Array<{
128
+ message: string;
129
+ location?: string;
130
+ }>;
131
+ }
132
+
133
+ /**
134
+ * Regular expression patterns for parsing AI responses
135
+ */
136
+ const AI_RESPONSE_PATTERNS = {
137
+ /**
138
+ * Matches YAML file blocks with explicit headers in the format:
139
+ * # filename.object.yml or File: filename.object.yml
140
+ * followed by a YAML code block
141
+ *
142
+ * Groups:
143
+ * 1. filename (e.g., "user.object.yml")
144
+ * 2. YAML content
145
+ */
146
+ FILE_BLOCK_YAML: /(?:^|\n)(?:#|File:)\s*([a-zA-Z0-9_-]+\.[a-z]+\.yml)\s*\n```(?:yaml|yml)?\n([\s\S]*?)```/gi,
147
+
148
+ /**
149
+ * Matches TypeScript file blocks with explicit headers in the format:
150
+ * // filename.action.ts or File: filename.hook.ts or filename.test.ts
151
+ * followed by a TypeScript code block
152
+ *
153
+ * Groups:
154
+ * 1. filename (e.g., "approve_order.action.ts", "user.hook.ts", "user.test.ts")
155
+ * 2. TypeScript content
156
+ */
157
+ FILE_BLOCK_TS: /(?:^|\n)(?:\/\/|File:)\s*([a-zA-Z0-9_-]+\.(?:action|hook|test|spec)\.ts)\s*\n```(?:typescript|ts)?\n([\s\S]*?)```/gi,
158
+
159
+ /**
160
+ * Matches generic YAML/YML code blocks without explicit headers
161
+ *
162
+ * Groups:
163
+ * 1. YAML content
164
+ */
165
+ CODE_BLOCK_YAML: /```(?:yaml|yml)\n([\s\S]*?)```/g,
166
+
167
+ /**
168
+ * Matches generic TypeScript code blocks without explicit headers
169
+ *
170
+ * Groups:
171
+ * 1. TypeScript content
172
+ */
173
+ CODE_BLOCK_TS: /```(?:typescript|ts)\n([\s\S]*?)```/g,
174
+ };
175
+
176
+ /**
177
+ * ObjectQL AI Agent for programmatic application generation and validation
178
+ */
179
+ export class ObjectQLAgent {
180
+ private openai: OpenAI;
181
+ private validator: Validator;
182
+ private config: Required<AgentConfig>;
183
+
184
+ constructor(config: AgentConfig) {
185
+ this.config = {
186
+ apiKey: config.apiKey,
187
+ model: config.model || 'gpt-4',
188
+ temperature: config.temperature ?? 0.7,
189
+ language: config.language || 'en',
190
+ };
191
+
192
+ this.openai = new OpenAI({ apiKey: this.config.apiKey });
193
+ this.validator = new Validator({ language: this.config.language });
194
+ }
195
+
196
+ /**
197
+ * Generate application metadata from natural language description
198
+ */
199
+ async generateApp(options: GenerateAppOptions): Promise<GenerateAppResult> {
200
+ const systemPrompt = this.getSystemPrompt();
201
+ const userPrompt = this.buildGenerationPrompt(options);
202
+
203
+ try {
204
+ const completion = await this.openai.chat.completions.create({
205
+ model: this.config.model,
206
+ messages: [
207
+ { role: 'system', content: systemPrompt },
208
+ { role: 'user', content: userPrompt }
209
+ ],
210
+ temperature: this.config.temperature,
211
+ max_tokens: options.maxTokens || 4000,
212
+ });
213
+
214
+ const response = completion.choices[0]?.message?.content;
215
+ if (!response) {
216
+ return {
217
+ success: false,
218
+ files: [],
219
+ errors: ['No response from AI model'],
220
+ };
221
+ }
222
+
223
+ // Parse response and extract files
224
+ const files = this.parseGenerationResponse(response);
225
+
226
+ return {
227
+ success: files.length > 0,
228
+ files,
229
+ rawResponse: response,
230
+ errors: files.length === 0 ? ['Failed to extract metadata files from response'] : undefined,
231
+ };
232
+
233
+ } catch (error) {
234
+ return {
235
+ success: false,
236
+ files: [],
237
+ errors: [error instanceof Error ? error.message : 'Unknown error'],
238
+ };
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Validate metadata using AI
244
+ */
245
+ async validateMetadata(options: ValidateMetadataOptions): Promise<ValidateMetadataResult> {
246
+ // Parse metadata if it's a string
247
+ let parsedMetadata: any;
248
+ if (typeof options.metadata === 'string') {
249
+ try {
250
+ parsedMetadata = yaml.load(options.metadata);
251
+ } catch (error) {
252
+ return {
253
+ valid: false,
254
+ errors: [{
255
+ message: `YAML parsing error: ${error instanceof Error ? error.message : 'Invalid YAML'}`,
256
+ location: 'root',
257
+ }],
258
+ warnings: [],
259
+ info: [],
260
+ };
261
+ }
262
+ } else {
263
+ parsedMetadata = options.metadata;
264
+ }
265
+
266
+ // Build validation prompt
267
+ const validationPrompt = this.buildValidationPrompt(options);
268
+
269
+ try {
270
+ const completion = await this.openai.chat.completions.create({
271
+ model: this.config.model,
272
+ messages: [
273
+ { role: 'system', content: this.getValidationSystemPrompt() },
274
+ { role: 'user', content: validationPrompt }
275
+ ],
276
+ temperature: 0.3, // Lower temperature for more consistent validation
277
+ max_tokens: 2000,
278
+ });
279
+
280
+ const feedback = completion.choices[0]?.message?.content || '';
281
+
282
+ // Parse feedback into structured result
283
+ return this.parseFeedback(feedback);
284
+
285
+ } catch (error) {
286
+ return {
287
+ valid: false,
288
+ errors: [{
289
+ message: `Validation error: ${error instanceof Error ? error.message : 'Unknown error'}`,
290
+ }],
291
+ warnings: [],
292
+ info: [],
293
+ };
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Refine existing metadata based on feedback
299
+ */
300
+ async refineMetadata(
301
+ metadata: string,
302
+ feedback: string,
303
+ iterations: number = 1
304
+ ): Promise<GenerateAppResult> {
305
+ const systemPrompt = this.getSystemPrompt();
306
+
307
+ let currentMetadata = metadata;
308
+ let messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
309
+ { role: 'system', content: systemPrompt },
310
+ { role: 'user', content: `Here is the current metadata:\n\n${metadata}\n\nPlease refine it based on this feedback: ${feedback}` }
311
+ ];
312
+
313
+ for (let i = 0; i < iterations; i++) {
314
+ try {
315
+ const completion = await this.openai.chat.completions.create({
316
+ model: this.config.model,
317
+ messages,
318
+ temperature: 0.5,
319
+ max_tokens: 4000,
320
+ });
321
+
322
+ const response = completion.choices[0]?.message?.content;
323
+ if (!response) break;
324
+
325
+ currentMetadata = response;
326
+ messages.push({ role: 'assistant', content: response });
327
+
328
+ // If this isn't the last iteration, validate and continue
329
+ if (i < iterations - 1) {
330
+ const validation = await this.validateMetadata({
331
+ metadata: response,
332
+ checkBusinessLogic: true,
333
+ });
334
+
335
+ if (validation.valid) {
336
+ break; // Stop if validation passes
337
+ }
338
+
339
+ // Add validation feedback for next iteration
340
+ const validationFeedback = [
341
+ ...validation.errors.map(e => `ERROR: ${e.message}`),
342
+ ...validation.warnings.map(w => `WARNING: ${w.message}`)
343
+ ].join('\n');
344
+
345
+ messages.push({
346
+ role: 'user',
347
+ content: `Please address these issues:\n${validationFeedback}`
348
+ });
349
+ }
350
+
351
+ } catch (error) {
352
+ return {
353
+ success: false,
354
+ files: [],
355
+ errors: [error instanceof Error ? error.message : 'Unknown error'],
356
+ };
357
+ }
358
+ }
359
+
360
+ const files = this.parseGenerationResponse(currentMetadata);
361
+
362
+ return {
363
+ success: files.length > 0,
364
+ files,
365
+ rawResponse: currentMetadata,
366
+ };
367
+ }
368
+
369
+ /**
370
+ * Conversational generation with step-by-step refinement
371
+ * This allows users to iteratively improve the application through dialogue
372
+ */
373
+ async generateConversational(
374
+ options: ConversationalGenerateOptions
375
+ ): Promise<ConversationalGenerateResult> {
376
+ const systemPrompt = this.getSystemPrompt();
377
+
378
+ // Initialize or continue conversation
379
+ const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
380
+ { role: 'system', content: systemPrompt }
381
+ ];
382
+
383
+ // Add conversation history if provided
384
+ if (options.conversationHistory) {
385
+ messages.push(...options.conversationHistory.filter(m => m.role !== 'system'));
386
+ }
387
+
388
+ // Build the user message
389
+ let userMessage = options.message;
390
+
391
+ // If there's a current app state, include it in context
392
+ if (options.currentApp && options.currentApp.files.length > 0) {
393
+ const currentState = options.currentApp.files
394
+ .map(f => `# ${f.filename}\n${f.content}`)
395
+ .join('\n\n---\n\n');
396
+
397
+ userMessage = `Current application state:\n\n${currentState}\n\n---\n\nUser request: ${options.message}\n\nPlease update the application according to the user's request. Provide the complete updated files.`;
398
+ }
399
+
400
+ messages.push({ role: 'user', content: userMessage });
401
+
402
+ try {
403
+ const completion = await this.openai.chat.completions.create({
404
+ model: this.config.model,
405
+ messages,
406
+ temperature: this.config.temperature,
407
+ max_tokens: 4000,
408
+ });
409
+
410
+ const response = completion.choices[0]?.message?.content;
411
+ if (!response) {
412
+ return {
413
+ success: false,
414
+ files: [],
415
+ conversationHistory: [...(options.conversationHistory || []),
416
+ { role: 'user', content: options.message }],
417
+ errors: ['No response from AI model'],
418
+ };
419
+ }
420
+
421
+ // Parse the response
422
+ const files = this.parseGenerationResponse(response);
423
+
424
+ // Update conversation history
425
+ const updatedHistory: ConversationMessage[] = [
426
+ ...(options.conversationHistory || []),
427
+ { role: 'user', content: options.message },
428
+ { role: 'assistant', content: response }
429
+ ];
430
+
431
+ // Generate suggestions for next steps
432
+ const suggestions = this.generateSuggestions(files, options.currentApp);
433
+
434
+ return {
435
+ success: files.length > 0,
436
+ files,
437
+ rawResponse: response,
438
+ conversationHistory: updatedHistory,
439
+ suggestions,
440
+ errors: files.length === 0 ? ['Failed to extract metadata files from response'] : undefined,
441
+ };
442
+
443
+ } catch (error) {
444
+ return {
445
+ success: false,
446
+ files: [],
447
+ conversationHistory: [...(options.conversationHistory || []),
448
+ { role: 'user', content: options.message }],
449
+ errors: [error instanceof Error ? error.message : 'Unknown error'],
450
+ };
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Generate suggestions for next steps based on current application state
456
+ */
457
+ private generateSuggestions(
458
+ currentFiles: GenerateAppResult['files'],
459
+ previousApp?: GenerateAppResult
460
+ ): string[] {
461
+ const suggestions: string[] = [];
462
+
463
+ // Check what metadata types are missing
464
+ const fileTypes = new Set(currentFiles.map(f => f.type));
465
+
466
+ const allTypes = [
467
+ 'object', 'validation', 'form', 'view', 'page',
468
+ 'menu', 'action', 'hook', 'permission', 'workflow', 'report', 'data'
469
+ ];
470
+
471
+ const missingTypes = allTypes.filter(t => !fileTypes.has(t as any));
472
+
473
+ if (missingTypes.length > 0) {
474
+ suggestions.push(`Consider adding: ${missingTypes.join(', ')}`);
475
+ }
476
+
477
+ if (!fileTypes.has('permission')) {
478
+ suggestions.push('Add permissions to control access');
479
+ }
480
+
481
+ if (!fileTypes.has('menu')) {
482
+ suggestions.push('Create a menu for navigation');
483
+ }
484
+
485
+ if (!fileTypes.has('workflow') && fileTypes.has('object')) {
486
+ suggestions.push('Add workflows for approval processes');
487
+ }
488
+
489
+ if (!fileTypes.has('report') && fileTypes.has('object')) {
490
+ suggestions.push('Generate reports for analytics');
491
+ }
492
+
493
+ return suggestions;
494
+ }
495
+
496
+ /**
497
+ * Get system prompt for metadata generation
498
+ */
499
+ private getSystemPrompt(): string {
500
+ return `You are an expert ObjectQL architect. Generate valid ObjectQL metadata in YAML format AND TypeScript implementation files for business logic.
501
+
502
+ Follow ObjectQL metadata standards for ALL metadata types:
503
+
504
+ **1. Core Data Layer:**
505
+ - Objects (*.object.yml): entities, fields, relationships, indexes
506
+ - Validations (*.validation.yml): validation rules, business constraints
507
+ - Data (*.data.yml): seed data and initial records
508
+
509
+ **2. Business Logic Layer (YAML + TypeScript):**
510
+ - Actions (*.action.yml + *.action.ts): custom RPC operations with TypeScript implementation
511
+ - Hooks (*.hook.yml + *.hook.ts): lifecycle triggers with TypeScript implementation
512
+ - beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete
513
+ - Workflows (*.workflow.yml): approval processes, automation
514
+
515
+ **3. Presentation Layer:**
516
+ - Pages (*.page.yml): composable UI pages with layouts
517
+ - Views (*.view.yml): list views, kanban, calendar displays
518
+ - Forms (*.form.yml): data entry forms with field layouts
519
+ - Reports (*.report.yml): tabular, summary, matrix reports
520
+ - Menus (*.menu.yml): navigation structure
521
+
522
+ **4. Security Layer:**
523
+ - Permissions (*.permission.yml): access control rules
524
+ - Application (*.application.yml): app-level configuration
525
+
526
+ **Field Types:** text, number, boolean, select, date, datetime, lookup, currency, email, phone, url, textarea, formula, file, image
527
+
528
+ **For Actions - Generate BOTH files:**
529
+ Example:
530
+ # approve_order.action.yml
531
+ \`\`\`yaml
532
+ label: Approve Order
533
+ type: record
534
+ params:
535
+ comment:
536
+ type: textarea
537
+ label: Comment
538
+ \`\`\`
539
+
540
+ # approve_order.action.ts
541
+ \`\`\`typescript
542
+ import { ActionContext } from '@objectql/types';
543
+
544
+ export default async function approveOrder(context: ActionContext) {
545
+ const { recordId, params, user, app } = context;
546
+
547
+ // Business logic here
548
+ const record = await app.findOne('orders', { _id: recordId });
549
+
550
+ if (!record) {
551
+ throw new Error('Order not found');
552
+ }
553
+
554
+ await app.update('orders', recordId, {
555
+ status: 'approved',
556
+ approved_by: user.id,
557
+ approved_at: new Date(),
558
+ approval_comment: params.comment
559
+ });
560
+
561
+ return { success: true, message: 'Order approved successfully' };
562
+ }
563
+ \`\`\`
564
+
565
+ **For Hooks - Generate BOTH files:**
566
+ Example:
567
+ # user.hook.yml
568
+ \`\`\`yaml
569
+ triggers:
570
+ - beforeCreate
571
+ - beforeUpdate
572
+ \`\`\`
573
+
574
+ # user.hook.ts
575
+ \`\`\`typescript
576
+ import { HookContext } from '@objectql/types';
577
+
578
+ export async function beforeCreate(context: HookContext) {
579
+ const { data, user } = context;
580
+
581
+ // Auto-assign creator
582
+ data.created_by = user.id;
583
+ data.created_at = new Date();
584
+
585
+ // Validate email uniqueness (example)
586
+ const existing = await context.app.findOne('users', { email: data.email });
587
+ if (existing) {
588
+ throw new Error('Email already exists');
589
+ }
590
+ }
591
+
592
+ export async function beforeUpdate(context: HookContext) {
593
+ const { data, previousData, user } = context;
594
+
595
+ data.updated_by = user.id;
596
+ data.updated_at = new Date();
597
+ }
598
+ \`\`\`
599
+
600
+ **For Tests - Generate test files:**
601
+ Example:
602
+ // approve_order.test.ts
603
+ \`\`\`typescript
604
+ import { describe, it, expect, beforeEach } from '@jest/globals';
605
+ import { ObjectQL } from '@objectql/core';
606
+ import approveOrder from './approve_order.action';
607
+
608
+ describe('approve_order action', () => {
609
+ let app: ObjectQL;
610
+ let testUser: any;
611
+
612
+ beforeEach(async () => {
613
+ // Setup test environment
614
+ app = new ObjectQL(/* test config */);
615
+ await app.connect();
616
+ testUser = { id: 'user123', name: 'Test User' };
617
+ });
618
+
619
+ it('should approve an order successfully', async () => {
620
+ // Create test order
621
+ const order = await app.create('orders', {
622
+ status: 'pending',
623
+ total: 100
624
+ });
625
+
626
+ // Execute action
627
+ const result = await approveOrder({
628
+ recordId: order.id,
629
+ params: { comment: 'Approved' },
630
+ user: testUser,
631
+ app
632
+ });
633
+
634
+ // Verify
635
+ expect(result.success).toBe(true);
636
+ const updated = await app.findOne('orders', { _id: order.id });
637
+ expect(updated.status).toBe('approved');
638
+ });
639
+
640
+ it('should reject if order not found', async () => {
641
+ await expect(approveOrder({
642
+ recordId: 'invalid_id',
643
+ params: { comment: 'Test' },
644
+ user: testUser,
645
+ app
646
+ })).rejects.toThrow('Order not found');
647
+ });
648
+ });
649
+ \`\`\`
650
+
651
+ **Best Practices:**
652
+ - Use snake_case for names
653
+ - Clear, business-friendly labels
654
+ - Include validation rules
655
+ - Add help text for clarity
656
+ - Define proper relationships
657
+ - Consider security from the start
658
+ - Implement actual business logic in TypeScript files
659
+ - Include error handling in implementations
660
+ - Add comments explaining complex logic
661
+ - Write comprehensive tests for all business logic
662
+ - Test both success and failure cases
663
+
664
+ Output format: Provide each file in code blocks with filename headers (e.g., "# filename.object.yml" or "// filename.action.ts").`;
665
+ }
666
+
667
+ /**
668
+ * Get system prompt for validation
669
+ */
670
+ private getValidationSystemPrompt(): string {
671
+ return `You are an expert ObjectQL metadata validator. Analyze metadata for:
672
+ 1. YAML structure and syntax
673
+ 2. ObjectQL specification compliance
674
+ 3. Business logic consistency
675
+ 4. Data modeling best practices
676
+ 5. Security considerations
677
+ 6. Performance implications
678
+
679
+ Provide feedback in this format:
680
+ - [ERROR] Location: Issue description
681
+ - [WARNING] Location: Issue description
682
+ - [INFO] Location: Suggestion`;
683
+ }
684
+
685
+ /**
686
+ * Build generation prompt based on options
687
+ */
688
+ private buildGenerationPrompt(options: GenerateAppOptions): string {
689
+ const { description, type = 'custom' } = options;
690
+
691
+ switch (type) {
692
+ case 'basic':
693
+ return `Generate a minimal ObjectQL application for: ${description}
694
+
695
+ Include:
696
+ - 2-3 core objects with essential fields
697
+ - Basic relationships between objects
698
+ - Simple validation rules
699
+ - At least one form and view per object
700
+ - At least one action with TypeScript implementation
701
+ - At least one hook with TypeScript implementation
702
+
703
+ Output: Provide each file separately with clear filename headers (e.g., "# filename.object.yml" or "// filename.action.ts").`;
704
+
705
+ case 'complete':
706
+ return `Generate a complete ObjectQL enterprise application for: ${description}
707
+
708
+ Include ALL necessary metadata types WITH implementations:
709
+ 1. **Objects**: All entities with comprehensive fields
710
+ 2. **Validations**: Business rules and constraints
711
+ 3. **Forms**: Create and edit forms for each object
712
+ 4. **Views**: List views for browsing data
713
+ 5. **Pages**: Dashboard and detail pages
714
+ 6. **Menus**: Navigation structure
715
+ 7. **Actions WITH TypeScript implementations**: Common operations (approve, export, etc.) - Generate BOTH .yml metadata AND .action.ts implementation files
716
+ 8. **Hooks WITH TypeScript implementations**: Lifecycle triggers - Generate .hook.ts implementation files
717
+ 9. **Permissions**: Basic access control
718
+ 10. **Data**: Sample seed data (optional)
719
+ 11. **Workflows**: Approval processes if applicable
720
+ 12. **Reports**: Key reports for analytics
721
+ 13. **Tests**: Generate test files (.test.ts) for actions and hooks to validate business logic
722
+
723
+ Consider:
724
+ - Security and permissions from the start
725
+ - User experience in form/view design
726
+ - Business processes and workflows
727
+ - Data integrity and validation
728
+ - Complete TypeScript implementations for all actions and hooks
729
+ - Test coverage for business logic
730
+
731
+ Output: Provide each file separately with clear filename headers (e.g., "# filename.object.yml" or "// filename.action.ts").`;
732
+
733
+ default:
734
+ return `Generate ObjectQL metadata for: ${description}
735
+
736
+ Analyze the requirements and create appropriate metadata across ALL relevant types:
737
+ - Objects, Validations, Forms, Views, Pages, Menus, Actions, Hooks, Permissions, Workflows, Reports, Data, Application
738
+ - For Actions and Hooks: Generate BOTH YAML metadata AND TypeScript implementation files
739
+ - Include test files to validate business logic
740
+
741
+ Output: Provide each file separately with clear filename headers (e.g., "# filename.object.yml" or "// filename.action.ts").`;
742
+ }
743
+ }
744
+
745
+ /**
746
+ * Build validation prompt
747
+ */
748
+ private buildValidationPrompt(options: ValidateMetadataOptions): string {
749
+ const metadataStr = typeof options.metadata === 'string'
750
+ ? options.metadata
751
+ : yaml.dump(options.metadata);
752
+
753
+ const checks = [];
754
+ if (options.checkBusinessLogic !== false) checks.push('Business logic consistency');
755
+ if (options.checkPerformance) checks.push('Performance considerations');
756
+ if (options.checkSecurity) checks.push('Security issues');
757
+
758
+ return `Validate this ObjectQL metadata file:
759
+
760
+ ${options.filename ? `Filename: ${options.filename}\n` : ''}
761
+ Content:
762
+ \`\`\`yaml
763
+ ${metadataStr}
764
+ \`\`\`
765
+
766
+ Check for:
767
+ - YAML syntax and structure
768
+ - ObjectQL specification compliance
769
+ ${checks.length > 0 ? '- ' + checks.join('\n- ') : ''}
770
+
771
+ Provide feedback in the specified format.`;
772
+ }
773
+
774
+ /**
775
+ * Parse generation response and extract files
776
+ */
777
+ private parseGenerationResponse(response: string): GenerateAppResult['files'] {
778
+ const files: GenerateAppResult['files'] = [];
779
+ let match;
780
+
781
+ // Extract YAML files with explicit headers
782
+ while ((match = AI_RESPONSE_PATTERNS.FILE_BLOCK_YAML.exec(response)) !== null) {
783
+ const filename = match[1];
784
+ const content = match[2].trim();
785
+ const type = this.inferFileType(filename);
786
+
787
+ files.push({ filename, content, type });
788
+ }
789
+
790
+ // Extract TypeScript files with explicit headers
791
+ while ((match = AI_RESPONSE_PATTERNS.FILE_BLOCK_TS.exec(response)) !== null) {
792
+ const filename = match[1];
793
+ const content = match[2].trim();
794
+ const type = this.inferFileType(filename);
795
+
796
+ files.push({ filename, content, type });
797
+ }
798
+
799
+ // Fallback: Generic code blocks if no explicit headers found
800
+ if (files.length === 0) {
801
+ let yamlIndex = 0;
802
+ let tsIndex = 0;
803
+
804
+ // Try to extract generic YAML blocks
805
+ while ((match = AI_RESPONSE_PATTERNS.CODE_BLOCK_YAML.exec(response)) !== null) {
806
+ const content = match[1].trim();
807
+ const filename = `generated_${yamlIndex}.object.yml`;
808
+
809
+ files.push({ filename, content, type: 'object' });
810
+ yamlIndex++;
811
+ }
812
+
813
+ // Try to extract generic TypeScript blocks
814
+ while ((match = AI_RESPONSE_PATTERNS.CODE_BLOCK_TS.exec(response)) !== null) {
815
+ const content = match[1].trim();
816
+ // Use generic .ts extension since we can't determine the specific type
817
+ const filename = `generated_${tsIndex}.ts`;
818
+
819
+ files.push({ filename, content, type: 'typescript' });
820
+ tsIndex++;
821
+ }
822
+ }
823
+
824
+ return files;
825
+ }
826
+
827
+ /**
828
+ * Parse validation feedback into structured result
829
+ */
830
+ private parseFeedback(feedback: string): ValidateMetadataResult {
831
+ const errors: ValidateMetadataResult['errors'] = [];
832
+ const warnings: ValidateMetadataResult['warnings'] = [];
833
+ const info: ValidateMetadataResult['info'] = [];
834
+
835
+ const lines = feedback.split('\n');
836
+
837
+ for (let i = 0; i < lines.length; i++) {
838
+ const line = lines[i];
839
+
840
+ if (line.includes('[ERROR]')) {
841
+ const message = line.replace(/^\s*-?\s*\[ERROR\]\s*/, '');
842
+ errors.push({ message });
843
+ } else if (line.includes('[WARNING]')) {
844
+ const message = line.replace(/^\s*-?\s*\[WARNING\]\s*/, '');
845
+ warnings.push({ message });
846
+ } else if (line.includes('[INFO]')) {
847
+ const message = line.replace(/^\s*-?\s*\[INFO\]\s*/, '');
848
+ info.push({ message });
849
+ }
850
+ }
851
+
852
+ return {
853
+ valid: errors.length === 0,
854
+ errors,
855
+ warnings,
856
+ info,
857
+ };
858
+ }
859
+
860
+ /**
861
+ * Infer file type from filename
862
+ */
863
+ private inferFileType(filename: string): GenerateAppResult['files'][0]['type'] {
864
+ if (filename.includes('.object.yml')) return 'object';
865
+ if (filename.includes('.validation.yml')) return 'validation';
866
+ if (filename.includes('.form.yml')) return 'form';
867
+ if (filename.includes('.view.yml')) return 'view';
868
+ if (filename.includes('.page.yml')) return 'page';
869
+ if (filename.includes('.menu.yml')) return 'menu';
870
+ if (filename.includes('.action.yml')) return 'action';
871
+ if (filename.includes('.hook.yml')) return 'hook';
872
+ if (filename.includes('.permission.yml')) return 'permission';
873
+ if (filename.includes('.workflow.yml')) return 'workflow';
874
+ if (filename.includes('.report.yml')) return 'report';
875
+ if (filename.includes('.data.yml')) return 'data';
876
+ if (filename.includes('.application.yml') || filename.includes('.app.yml')) return 'application';
877
+ if (filename.includes('.action.ts') || filename.includes('.hook.ts')) return 'typescript';
878
+ if (filename.includes('.test.ts') || filename.includes('.spec.ts')) return 'test';
879
+ return 'other';
880
+ }
881
+ }
882
+
883
+ /**
884
+ * Convenience function to create an agent instance
885
+ */
886
+ export function createAgent(apiKey: string, options?: Partial<AgentConfig>): ObjectQLAgent {
887
+ return new ObjectQLAgent({ apiKey, ...options });
888
+ }