feat(weeks2-3): data ingestion, geospatial launcher, intelligence endpoints
This commit is contained in:
166
src/lib/data/dataValidator.ts
Normal file
166
src/lib/data/dataValidator.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Data Validation Schemas for God Mode
|
||||
* Uses Zod for runtime type safety
|
||||
*/
|
||||
|
||||
// ============================================================
|
||||
// INGESTION PAYLOAD SCHEMA
|
||||
// ============================================================
|
||||
|
||||
export const IngestionPayloadSchema = z.object({
|
||||
data: z.string().min(1, 'Data cannot be empty'),
|
||||
format: z.enum(['csv', 'json']),
|
||||
tableName: z.string()
|
||||
.regex(/^[a-zA-Z0-9_]+$/, 'Table name must be alphanumeric with underscores only')
|
||||
.min(1, 'Table name required'),
|
||||
columnMapping: z.record(z.string(), z.string()).optional(),
|
||||
validateOnly: z.boolean().optional().default(false)
|
||||
});
|
||||
|
||||
export type IngestionPayload = z.infer<typeof IngestionPayloadSchema>;
|
||||
|
||||
// ============================================================
|
||||
// TARGET ROW SCHEMAS (Flexible for different data types)
|
||||
// ============================================================
|
||||
|
||||
// Basic target schema (city-based content)
|
||||
export const CityTargetSchema = z.object({
|
||||
city_name: z.string().min(1),
|
||||
state: z.string().length(2).optional(),
|
||||
county: z.string().optional(),
|
||||
lat: z.number().min(-90).max(90).optional(),
|
||||
lng: z.number().min(-180).max(180).optional(),
|
||||
population: z.number().optional(),
|
||||
zip: z.string().optional()
|
||||
});
|
||||
|
||||
// Competitor URL target
|
||||
export const CompetitorTargetSchema = z.object({
|
||||
url: z.string().url(),
|
||||
domain: z.string(),
|
||||
industry: z.string().optional(),
|
||||
target_keywords: z.array(z.string()).optional()
|
||||
});
|
||||
|
||||
// Generic flexible target (catchall)
|
||||
export const GenericTargetSchema = z.record(z.string(), z.union([
|
||||
z.string(),
|
||||
z.number(),
|
||||
z.boolean(),
|
||||
z.null()
|
||||
]));
|
||||
|
||||
// ============================================================
|
||||
// GENERATION JOB SCHEMAS
|
||||
// ============================================================
|
||||
|
||||
export const GenerationJobDataSchema = z.object({
|
||||
job_type: z.enum(['generate_post', 'publish', 'assemble', 'geo_campaign']),
|
||||
site_id: z.string().uuid().optional(),
|
||||
target_data: z.record(z.any()),
|
||||
campaign_id: z.string().uuid().optional(),
|
||||
priority: z.number().min(0).max(10).default(5)
|
||||
});
|
||||
|
||||
export type GenerationJobData = z.infer<typeof GenerationJobDataSchema>;
|
||||
|
||||
// ============================================================
|
||||
// GEOSPATIAL SCHEMAS
|
||||
// ============================================================
|
||||
|
||||
export const GeoPointSchema = z.tuple([z.number(), z.number()]); // [lat, lng]
|
||||
|
||||
export const GeoBoundarySchema = z.object({
|
||||
type: z.literal('Polygon'),
|
||||
coordinates: z.array(z.array(GeoPointSchema))
|
||||
});
|
||||
|
||||
export const GeoCampaignSchema = z.object({
|
||||
boundary: GeoBoundarySchema,
|
||||
campaign_type: z.enum(['local_article', 'service_area', 'competitor_targeting']),
|
||||
density: z.enum(['low', 'medium', 'high', 'insane']).default('medium'),
|
||||
template_id: z.string().uuid().optional(),
|
||||
site_id: z.string().uuid(),
|
||||
target_count: z.number().min(1).max(100000).optional()
|
||||
});
|
||||
|
||||
export type GeoCampaign = z.infer<typeof GeoCampaignSchema>;
|
||||
|
||||
// ============================================================
|
||||
// PROMPT TESTING SCHEMA
|
||||
// ============================================================
|
||||
|
||||
export const PromptTestSchema = z.object({
|
||||
prompt: z.string().min(10, 'Prompt too short'),
|
||||
variables: z.record(z.string(), z.string()).default({}),
|
||||
model: z.enum(['gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo', 'claude-3-opus']).default('gpt-4'),
|
||||
max_tokens: z.number().min(100).max(8000).default(1000),
|
||||
temperature: z.number().min(0).max(2).default(0.7)
|
||||
});
|
||||
|
||||
export type PromptTest = z.infer<typeof PromptTestSchema>;
|
||||
|
||||
// ============================================================
|
||||
// SPINTAX VALIDATION SCHEMA
|
||||
// ============================================================
|
||||
|
||||
export const SpintaxPatternSchema = z.object({
|
||||
pattern: z.string().min(1),
|
||||
validate_recursion: z.boolean().default(true),
|
||||
max_depth: z.number().min(1).max(10).default(3)
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// SYSTEM CONFIG SCHEMA
|
||||
// ============================================================
|
||||
|
||||
export const SystemConfigSchema = z.object({
|
||||
throttle_delay_ms: z.number().min(0).max(10000).default(0),
|
||||
max_concurrency: z.number().min(1).max(1000).default(128),
|
||||
max_cost_per_hour: z.number().min(0).max(10000).default(100),
|
||||
enable_auto_throttle: z.boolean().default(true),
|
||||
memory_threshold_pct: z.number().min(50).max(99).default(90)
|
||||
});
|
||||
|
||||
export type SystemConfig = z.infer<typeof SystemConfigSchema>;
|
||||
|
||||
// ============================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Validate and parse ingestion payload
|
||||
*/
|
||||
export function validateIngestionPayload(payload: unknown): IngestionPayload {
|
||||
return IngestionPayloadSchema.parse(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate city target data
|
||||
*/
|
||||
export function validateCityTarget(data: unknown) {
|
||||
return CityTargetSchema.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate generation job data
|
||||
*/
|
||||
export function validateJobData(data: unknown): GenerationJobData {
|
||||
return GenerationJobDataSchema.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate geospatial campaign
|
||||
*/
|
||||
export function validateGeoCampaign(data: unknown): GeoCampaign {
|
||||
return GeoCampaignSchema.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate prompt test data
|
||||
*/
|
||||
export function validatePromptTest(data: unknown) {
|
||||
return PromptTestSchema.parse(data);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { pool } from './db';
|
||||
import { pool } from '../db';
|
||||
|
||||
/**
|
||||
* Migration System for God Mode
|
||||
@@ -90,7 +90,7 @@ export async function getMigrationStatus(): Promise<{
|
||||
`);
|
||||
|
||||
return {
|
||||
tables: result.rows.map(r => r.table_name)
|
||||
tables: result.rows.map((r: { table_name: string }) => r.table_name)
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
235
src/pages/api/god/data/ingest.ts
Normal file
235
src/pages/api/god/data/ingest.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import Papa from 'papaparse';
|
||||
import { pool } from '@/lib/db';
|
||||
import { validateIngestionPayload, GenericTargetSchema } from '@/lib/data/dataValidator';
|
||||
|
||||
/**
|
||||
* Data Ingestion Endpoint
|
||||
* Load 10k+ targets from CSV/JSON in seconds
|
||||
*/
|
||||
|
||||
function validateGodToken(request: Request): boolean {
|
||||
const token = request.headers.get('X-God-Token') ||
|
||||
request.headers.get('Authorization')?.replace('Bearer ', '') ||
|
||||
new URL(request.url).searchParams.get('token');
|
||||
|
||||
const godToken = process.env.GOD_MODE_TOKEN || import.meta.env.GOD_MODE_TOKEN;
|
||||
if (!godToken) return true;
|
||||
return token === godToken;
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
if (!validateGodToken(request)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Validate payload
|
||||
const payload = validateIngestionPayload(await request.json());
|
||||
let records: Record<string, any>[] = [];
|
||||
|
||||
// Step 2: Parse data (CSV or JSON)
|
||||
if (payload.format === 'csv') {
|
||||
const parseResult = Papa.parse(payload.data, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
dynamicTyping: true,
|
||||
transformHeader: (header) => header.trim().toLowerCase().replace(/\s+/g, '_')
|
||||
});
|
||||
|
||||
if (parseResult.errors.length > 0) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'CSV parsing failed',
|
||||
details: parseResult.errors.slice(0, 10)
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
records = parseResult.data as Record<string, any>[];
|
||||
|
||||
} else if (payload.format === 'json') {
|
||||
records = JSON.parse(payload.data);
|
||||
if (!Array.isArray(records)) {
|
||||
records = [records];
|
||||
}
|
||||
}
|
||||
|
||||
if (records.length === 0) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'No records found after parsing'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Step 3: Validate each record with Zod
|
||||
const validatedRecords = records.map((record, index) => {
|
||||
try {
|
||||
return GenericTargetSchema.parse(record);
|
||||
} catch (error: any) {
|
||||
throw new Error(`Record ${index + 1} validation failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`📋 [Ingestion] Parsed ${validatedRecords.length} records for table: ${payload.tableName}`);
|
||||
|
||||
// Step 4: Validate-only mode (dry run)
|
||||
if (payload.validateOnly) {
|
||||
return new Response(JSON.stringify({
|
||||
valid: true,
|
||||
recordCount: validatedRecords.length,
|
||||
columns: Object.keys(validatedRecords[0]),
|
||||
sample: validatedRecords.slice(0, 3),
|
||||
message: 'Validation successful - no data inserted (validateOnly=true)'
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Step 5: Apply column mapping if provided
|
||||
const mappedRecords = validatedRecords.map(record => {
|
||||
if (!payload.columnMapping) return record;
|
||||
|
||||
const mapped: Record<string, any> = {};
|
||||
for (const [sourceCol, targetCol] of Object.entries(payload.columnMapping)) {
|
||||
if (record[sourceCol] !== undefined) {
|
||||
mapped[targetCol] = record[sourceCol];
|
||||
}
|
||||
}
|
||||
return Object.keys(mapped).length > 0 ? mapped : record;
|
||||
});
|
||||
|
||||
// Step 6: Bulk INSERT in transaction
|
||||
const client = await pool.connect();
|
||||
let insertedCount = 0;
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
const columns = Object.keys(mappedRecords[0]);
|
||||
|
||||
// Batch inserts (1000 at a time for performance)
|
||||
const batchSize = 1000;
|
||||
for (let i = 0; i < mappedRecords.length; i += batchSize) {
|
||||
const batch = mappedRecords.slice(i, i + batchSize);
|
||||
|
||||
for (const record of batch) {
|
||||
const values = columns.map(col => record[col]);
|
||||
const placeholders = values.map((_, idx) => `$${idx + 1}`).join(', ');
|
||||
|
||||
const insertQuery = `
|
||||
INSERT INTO "${payload.tableName}" (${columns.join(', ')})
|
||||
VALUES (${placeholders})
|
||||
ON CONFLICT DO NOTHING
|
||||
`;
|
||||
|
||||
const result = await client.query(insertQuery, values);
|
||||
insertedCount += result.rowCount || 0;
|
||||
}
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
console.log(`✅ [Ingestion] Inserted ${insertedCount} records into ${payload.tableName}`);
|
||||
|
||||
} catch (error: any) {
|
||||
await client.query('ROLLBACK');
|
||||
throw new Error(`Database insertion failed: ${error.message}`);
|
||||
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
// Step 7: Success response
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
tableName: payload.tableName,
|
||||
recordsProcessed: validatedRecords.length,
|
||||
recordsInserted: insertedCount,
|
||||
recordsSkipped: validatedRecords.length - insertedCount,
|
||||
columns: Object.keys(mappedRecords[0]),
|
||||
timestamp: new Date().toISOString(),
|
||||
next_steps: {
|
||||
query_data: `SELECT * FROM ${payload.tableName} LIMIT 10`,
|
||||
push_to_queue: `Use POST /api/god/sql with push_to_queue flag`
|
||||
}
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ [Ingestion] Error:', error.message);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Ingestion failed',
|
||||
details: error.message,
|
||||
hint: 'Check that table exists and columns match your data'
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// GET for documentation
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
if (!validateGodToken(request)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
endpoint: 'POST /api/god/data/ingest',
|
||||
description: 'Bulk load targets from CSV or JSON',
|
||||
features: [
|
||||
'Parse CSV with papaparse',
|
||||
'Validate with Zod schemas',
|
||||
'Bulk INSERT in transaction',
|
||||
'Column mapping support',
|
||||
'Dry-run mode (validateOnly)'
|
||||
],
|
||||
usage: {
|
||||
csv_example: {
|
||||
format: 'csv',
|
||||
tableName: 'geo_locations',
|
||||
data: 'city_name,state,lat,lng\nAustin,TX,30.2672,-97.7431\nDallas,TX,32.7767,-96.7970',
|
||||
validateOnly: false
|
||||
},
|
||||
json_example: {
|
||||
format: 'json',
|
||||
tableName: 'sites',
|
||||
data: JSON.stringify([
|
||||
{ domain: 'example.com', name: 'Example Site' },
|
||||
{ domain: 'test.com', name: 'Test Site' }
|
||||
])
|
||||
},
|
||||
with_mapping: {
|
||||
format: 'csv',
|
||||
tableName: 'posts',
|
||||
data: 'Title,URL,Status\nTest Post,/test,draft',
|
||||
columnMapping: {
|
||||
'Title': 'title',
|
||||
'URL': 'slug',
|
||||
'Status': 'status'
|
||||
}
|
||||
}
|
||||
},
|
||||
limits: {
|
||||
max_records: 100000,
|
||||
batch_size: 1000,
|
||||
transaction_timeout: '30 seconds'
|
||||
}
|
||||
}, null, 2), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
||||
199
src/pages/api/god/geo/launch-campaign.ts
Normal file
199
src/pages/api/god/geo/launch-campaign.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import * as turf from '@turf/turf';
|
||||
import { pool } from '@/lib/db';
|
||||
import { validateGeoCampaign } from '@/lib/data/dataValidator';
|
||||
import { queues } from '@/lib/queue/config';
|
||||
|
||||
/**
|
||||
* Geospatial Job Launcher
|
||||
* Draw a boundary → generate thousands of location-based jobs
|
||||
*/
|
||||
|
||||
function validateGodToken(request: Request): boolean {
|
||||
const token = request.headers.get('X-God-Token') ||
|
||||
request.headers.get('Authorization')?.replace('Bearer ', '') ||
|
||||
new URL(request.url).searchParams.get('token');
|
||||
|
||||
const godToken = process.env.GOD_MODE_TOKEN || import.meta.env.GOD_MODE_TOKEN;
|
||||
if (!godToken) return true;
|
||||
return token === godToken;
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
if (!validateGodToken(request)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Validate campaign data
|
||||
const campaign = validateGeoCampaign(await request.json());
|
||||
|
||||
// Step 2: Calculate density and generate points
|
||||
const densityMap = {
|
||||
low: 10,
|
||||
medium: 50,
|
||||
high: 200,
|
||||
insane: 1000
|
||||
};
|
||||
|
||||
const pointsPerSquareMile = densityMap[campaign.density];
|
||||
|
||||
// Calculate area of boundary
|
||||
const polygon = turf.polygon(campaign.boundary.coordinates);
|
||||
const area = turf.area(polygon); // in square meters
|
||||
const areaMiles = area * 0.000000386102; // convert to square miles
|
||||
|
||||
const targetCount = campaign.target_count || Math.floor(areaMiles * pointsPerSquareMile);
|
||||
|
||||
console.log(`🌍 [Geo] Generating ${targetCount} points for ${areaMiles.toFixed(2)} sq mi at ${campaign.density} density`);
|
||||
|
||||
// Step 3: Generate random points within boundary using Turf.js
|
||||
const points = turf.randomPoint(targetCount, { bbox: turf.bbox(polygon) });
|
||||
|
||||
// Filter to only points actually inside the polygon
|
||||
const pointsInside = points.features.filter(point => {
|
||||
return turf.booleanPointInPolygon(point, polygon);
|
||||
});
|
||||
|
||||
console.log(`🌍 [Geo] ${pointsInside.length} points inside boundary`);
|
||||
|
||||
// Step 4: Insert points into geo_locations table
|
||||
const client = await pool.connect();
|
||||
let insertedCount = 0;
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
for (const point of pointsInside) {
|
||||
const [lng, lat] = point.geometry.coordinates;
|
||||
|
||||
const insertQuery = `
|
||||
INSERT INTO geo_locations (location, created_at)
|
||||
VALUES (ST_SetSRID(ST_MakePoint($1, $2), 4326), NOW())
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
await client.query(insertQuery, [lng, lat]);
|
||||
insertedCount++;
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
console.log(`✅ [Geo] Inserted ${insertedCount} locations`);
|
||||
|
||||
} catch (error: any) {
|
||||
await client.query('ROLLBACK');
|
||||
throw new Error(`Database error: ${error.message}`);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
// Step 5: Create generation jobs and push to BullMQ
|
||||
const jobs = pointsInside.map((point, index) => {
|
||||
const [lng, lat] = point.geometry.coordinates;
|
||||
|
||||
return {
|
||||
name: `geo-campaign-${campaign.site_id}-${index}`,
|
||||
data: {
|
||||
job_type: 'geo_campaign',
|
||||
site_id: campaign.site_id,
|
||||
campaign_id: campaign.campaign_type,
|
||||
target_data: {
|
||||
lat,
|
||||
lng,
|
||||
template_id: campaign.template_id
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Use addBulk for efficiency
|
||||
const addedJobs = await queues.generation.addBulk(jobs);
|
||||
|
||||
console.log(`📋 [Queue] Added ${addedJobs.length} jobs to generation queue`);
|
||||
|
||||
// Step 6: Return success
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
campaign: {
|
||||
type: campaign.campaign_type,
|
||||
density: campaign.density,
|
||||
site_id: campaign.site_id
|
||||
},
|
||||
area_sq_miles: areaMiles,
|
||||
points_generated: pointsInside.length,
|
||||
locations_inserted: insertedCount,
|
||||
jobs_queued: addedJobs.length,
|
||||
queue_ids: addedJobs.slice(0, 10).map(j => j.id),
|
||||
timestamp: new Date().toISOString(),
|
||||
next_steps: {
|
||||
monitor_queue: 'GET /api/god/queue/status',
|
||||
view_locations: `SELECT * FROM geo_locations ORDER BY created_at DESC LIMIT 100`
|
||||
}
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ [Geo] Campaign launch failed:', error.message);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Campaign launch failed',
|
||||
details: error.message
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// GET for documentation
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
if (!validateGodToken(request)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
endpoint: 'POST /api/god/geo/launch-campaign',
|
||||
description: 'Generate thousands of location-based content jobs from a map boundary',
|
||||
features: [
|
||||
'Uses @turf/turf for geospatial sampling',
|
||||
'Generates points based on density setting',
|
||||
'Inserts locations into database',
|
||||
'Pushes jobs to BullMQ with addBulk'
|
||||
],
|
||||
density_levels: {
|
||||
low: '10 points per square mile',
|
||||
medium: '50 points per square mile',
|
||||
high: '200 points per square mile',
|
||||
insane: '1000 points per square mile'
|
||||
},
|
||||
usage_example: {
|
||||
boundary: {
|
||||
type: 'Polygon',
|
||||
coordinates: [
|
||||
[
|
||||
[-97.74, 30.27],
|
||||
[-97.74, 30.40],
|
||||
[-97.54, 30.40],
|
||||
[-97.54, 30.27],
|
||||
[-97.74, 30.27]
|
||||
]
|
||||
]
|
||||
},
|
||||
campaign_type: 'local_article',
|
||||
density: 'medium',
|
||||
site_id: 'uuid-here',
|
||||
template_id: 'optional-uuid'
|
||||
}
|
||||
}, null, 2), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
||||
87
src/pages/api/god/pool/stats.ts
Normal file
87
src/pages/api/god/pool/stats.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { pool } from '@/lib/db';
|
||||
|
||||
/**
|
||||
* Connection Pool Statistics
|
||||
* Monitor database connection saturation
|
||||
*/
|
||||
|
||||
function validateGodToken(request: Request): boolean {
|
||||
const token = request.headers.get('X-God-Token') ||
|
||||
request.headers.get('Authorization')?.replace('Bearer ', '') ||
|
||||
new URL(request.url).searchParams.get('token');
|
||||
|
||||
const godToken = process.env.GOD_MODE_TOKEN || import.meta.env.GOD_MODE_TOKEN;
|
||||
if (!godToken) return true;
|
||||
return token === godToken;
|
||||
}
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
if (!validateGodToken(request)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Get pool stats
|
||||
const totalCount = pool.totalCount;
|
||||
const idleCount = pool.idleCount;
|
||||
const waitingCount = pool.waitingCount;
|
||||
|
||||
// Calculate saturation
|
||||
const saturation_pct = totalCount > 0
|
||||
? Math.round(((totalCount - idleCount) / totalCount) * 100 * 10) / 10
|
||||
: 0;
|
||||
|
||||
// Get active connections from database
|
||||
const activeConnections = await pool.query(`
|
||||
SELECT
|
||||
count(*) FILTER (WHERE state = 'active') as active,
|
||||
count(*) FILTER (WHERE state = 'idle') as idle,
|
||||
count(*) FILTER (WHERE state = 'idle in transaction') as idle_in_transaction,
|
||||
count(*) as total
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = current_database()
|
||||
`);
|
||||
|
||||
const connStats = activeConnections.rows[0];
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
pool: {
|
||||
total: totalCount,
|
||||
idle: idleCount,
|
||||
waiting: waitingCount,
|
||||
active: totalCount - idleCount,
|
||||
saturation_pct
|
||||
},
|
||||
database: {
|
||||
active: parseInt(connStats.active),
|
||||
idle: parseInt(connStats.idle),
|
||||
idle_in_transaction: parseInt(connStats.idle_in_transaction),
|
||||
total: parseInt(connStats.total)
|
||||
},
|
||||
health: {
|
||||
status: saturation_pct > 90 ? '⚠️ CRITICAL' : saturation_pct > 75 ? '⚠️ WARNING' : '✅ HEALTHY',
|
||||
recommendation: saturation_pct > 90
|
||||
? 'Increase pool size or kill stuck queries'
|
||||
: saturation_pct > 75
|
||||
? 'Monitor closely, consider throttling'
|
||||
: 'Pool operating normally'
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
return new Response(JSON.stringify({
|
||||
error: error.message
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
174
src/pages/api/god/shim/preview.ts
Normal file
174
src/pages/api/god/shim/preview.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
|
||||
/**
|
||||
* Sh im Preview - SQL Dry Run
|
||||
* Translate Directus SDK queries to SQL without executing
|
||||
*/
|
||||
|
||||
function validateGodToken(request: Request): boolean {
|
||||
const token = request.headers.get('X-God-Token') ||
|
||||
request.headers.get('Authorization')?.replace('Bearer ', '') ||
|
||||
new URL(request.url).searchParams.get('token');
|
||||
|
||||
const godToken = process.env.GOD_MODE_TOKEN || import.meta.env.GOD_MODE_TOKEN;
|
||||
if (!godToken) return true;
|
||||
return token === godToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic Directus query to SQL translation
|
||||
* This is a simplified version - full implementation in client.ts
|
||||
*/
|
||||
function translateToSQL(collection: string, query: any): string {
|
||||
const parts: string[] = [`SELECT * FROM "${collection}"`];
|
||||
|
||||
// WHERE clause from filter
|
||||
if (query.filter) {
|
||||
const conditions = buildConditions(query.filter);
|
||||
if (conditions) {
|
||||
parts.push(`WHERE ${conditions}`);
|
||||
}
|
||||
}
|
||||
|
||||
// LIMIT
|
||||
if (query.limit) {
|
||||
parts.push(`LIMIT ${query.limit}`);
|
||||
}
|
||||
|
||||
// OFFSET
|
||||
if (query.offset) {
|
||||
parts.push(`OFFSET ${query.offset}`);
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
function buildConditions(filter: any): string {
|
||||
const conditions: string[] = [];
|
||||
|
||||
for (const [field, value] of Object.entries(filter)) {
|
||||
if (field === '_or') {
|
||||
const orConditions = (value as any[]).map(buildConditions).filter(Boolean);
|
||||
if (orConditions.length > 0) {
|
||||
conditions.push(`(${orConditions.join(' OR ')})`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (field === '_and') {
|
||||
const andConditions = (value as any[]).map(buildConditions).filter(Boolean);
|
||||
if (andConditions.length > 0) {
|
||||
conditions.push(`(${andConditions.join(' AND ')})`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle operators
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
for (const [op, val] of Object.entries(value)) {
|
||||
switch (op) {
|
||||
case '_eq':
|
||||
conditions.push(`"${field}" = '${val}'`);
|
||||
break;
|
||||
case '_neq':
|
||||
conditions.push(`"${field}" != '${val}'`);
|
||||
break;
|
||||
case '_gt':
|
||||
conditions.push(`"${field}" > ${val}`);
|
||||
break;
|
||||
case '_lt':
|
||||
conditions.push(`"${field}" < ${val}`);
|
||||
break;
|
||||
case '_contains':
|
||||
conditions.push(`"${field}" ILIKE '%${val}%'`);
|
||||
break;
|
||||
case '_in':
|
||||
const vals = (val as any[]).map(v => `'${v}'`).join(', ');
|
||||
conditions.push(`"${field}" IN (${vals})`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Simple equality
|
||||
conditions.push(`"${field}" = '${value}'`);
|
||||
}
|
||||
}
|
||||
|
||||
return conditions.join(' AND ');
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
if (!validateGodToken(request)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const { collection, query } = await request.json();
|
||||
|
||||
if (!collection) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Missing collection name'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Translate to SQL
|
||||
const sql = translateToSQL(collection, query || {});
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
collection,
|
||||
query: query || {},
|
||||
sql,
|
||||
executed: false,
|
||||
note: 'This is a dry-run preview only. Use POST /api/god/sql to execute.'
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Translation failed',
|
||||
details: error.message
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
if (!validateGodToken(request)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
endpoint: 'POST /api/god/shim/preview',
|
||||
description: 'Preview SQL translation without executing',
|
||||
usage: {
|
||||
example: {
|
||||
collection: 'posts',
|
||||
query: {
|
||||
filter: {
|
||||
status: { _eq: 'published' },
|
||||
title: {
|
||||
_contains: 'Austin'
|
||||
}
|
||||
},
|
||||
limit: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}, null, 2), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
||||
152
src/pages/api/intelligence/prompts/test.ts
Normal file
152
src/pages/api/intelligence/prompts/test.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { validatePromptTest } from '@/lib/data/dataValidator';
|
||||
|
||||
/**
|
||||
* Prompt Testing Sandbox
|
||||
* Test prompts before burning $$$ on batch jobs
|
||||
*/
|
||||
|
||||
function validateGodToken(request: Request): boolean {
|
||||
const token = request.headers.get('X-God-Token') ||
|
||||
request.headers.get('Authorization')?.replace('Bearer ', '') ||
|
||||
new URL(request.url).searchParams.get('token');
|
||||
|
||||
const godToken = process.env.GOD_MODE_TOKEN || import.meta.env.GOD_MODE_TOKEN;
|
||||
if (!godToken) return true;
|
||||
return token === godToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate cost based on tokens
|
||||
*/
|
||||
function estimateCost(model: string, inputTokens: number, outputTokens: number): number {
|
||||
const pricing: Record<string, { input: number; output: number }> = {
|
||||
'gpt-4': { input: 0.03 / 1000, output: 0.06 / 1000 },
|
||||
'gpt-4-turbo': { input: 0.01 / 1000, output: 0.03 / 1000 },
|
||||
'gpt-3.5-turbo': { input: 0.0005 / 1000, output: 0.0015 / 1000 },
|
||||
'claude-3-opus': { input: 0.015 / 1000, output: 0.075 / 1000 }
|
||||
};
|
||||
|
||||
const prices = pricing[model] || pricing['gpt-4'];
|
||||
return (inputTokens * prices.input) + (outputTokens * prices.output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rough token estimation (4 chars ≈ 1 token)
|
||||
*/
|
||||
function estimateTokens(text: string): number {
|
||||
return Math.ceil(text.length / 4);
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
if (!validateGodToken(request)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const testData = validatePromptTest(await request.json());
|
||||
|
||||
// Replace variables in prompt
|
||||
let finalPrompt = testData.prompt;
|
||||
for (const [key, value] of Object.entries(testData.variables)) {
|
||||
finalPrompt = finalPrompt.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
|
||||
}
|
||||
|
||||
const inputTokens = estimateTokens(finalPrompt);
|
||||
|
||||
// Simulate LLM call (in production, would call OpenAI/Anthropic API)
|
||||
const mockResponse = {
|
||||
content: `[MOCK RESPONSE]\n\nPrompt received: ${finalPrompt.substring(0, 100)}...\n\nThis is a sandbox test. To enable real LLM calls, configure OPENAI_API_KEY or ANTHROPIC_API_KEY in environment variables.`,
|
||||
tokens_used: {
|
||||
input: inputTokens,
|
||||
output: estimateTokens("Mock response placeholder"),
|
||||
total: inputTokens + 50
|
||||
}
|
||||
};
|
||||
|
||||
const estimatedCost = estimateCost(
|
||||
testData.model,
|
||||
mockResponse.tokens_used.input,
|
||||
mockResponse.tokens_used.output
|
||||
);
|
||||
|
||||
// Calculate batch cost if user runs 10k jobs
|
||||
const batchCosts = {
|
||||
'100': estimatedCost * 100,
|
||||
'1000': estimatedCost * 1000,
|
||||
'10000': estimatedCost * 10000,
|
||||
'100000': estimatedCost * 100000
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
test: {
|
||||
model: testData.model,
|
||||
temperature: testData.temperature,
|
||||
max_tokens: testData.max_tokens
|
||||
},
|
||||
prompt: {
|
||||
original: testData.prompt,
|
||||
final: finalPrompt,
|
||||
variables_replaced: Object.keys(testData.variables).length
|
||||
},
|
||||
response: mockResponse.content,
|
||||
tokens: mockResponse.tokens_used,
|
||||
cost: {
|
||||
this_request: `$${estimatedCost.toFixed(6)}`,
|
||||
batch_estimates: {
|
||||
'100_jobs': `$${batchCosts['100'].toFixed(2)}`,
|
||||
' 1000_jobs': `$${batchCosts['1000'].toFixed(2)}`,
|
||||
'10000_jobs': `$${batchCosts['10000'].toFixed(2)}`,
|
||||
'100000_jobs': `$${batchCosts['100000'].toFixed(2)}`
|
||||
},
|
||||
warning: batchCosts['10000'] > 1000 ? '⚠️ 10k batch would cost > $1000!' : null
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Prompt test failed',
|
||||
details: error.message
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
if (!validateGodToken(request)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
endpoint: 'POST /api/intelligence/prompts/test',
|
||||
description: 'Test prompts and estimate costs before batch execution',
|
||||
usage: {
|
||||
example: {
|
||||
prompt: 'Write a blog post about {topic} in {city}, {state}',
|
||||
variables: {
|
||||
topic: 'local restaurants',
|
||||
city: 'Austin',
|
||||
state: 'TX'
|
||||
},
|
||||
model: 'gpt-4',
|
||||
max_tokens: 1000,
|
||||
temperature: 0.7
|
||||
}
|
||||
}
|
||||
}, null, 2), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
||||
219
src/pages/api/intelligence/spintax/validate.ts
Normal file
219
src/pages/api/intelligence/spintax/validate.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
|
||||
/**
|
||||
* Spintax Pattern Validator
|
||||
* Check syntax before running 10k generations
|
||||
*/
|
||||
|
||||
function validateGodToken(request: Request): boolean {
|
||||
const token = request.headers.get('X-God-Token') ||
|
||||
request.headers.get('Authorization')?.replace('Bearer ', '') ||
|
||||
new URL(request.url).searchParams.get('token');
|
||||
|
||||
const godToken = process.env.GOD_MODE_TOKEN || import.meta.env.GOD_MODE_TOKEN;
|
||||
if (!godToken) return true;
|
||||
return token === godToken;
|
||||
}
|
||||
|
||||
interface ValidationError {
|
||||
type: string;
|
||||
position: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate spintax pattern syntax
|
||||
*/
|
||||
function validateSpintax(pattern: string): { valid: boolean; errors: ValidationError[] } {
|
||||
const errors: ValidationError[] = [];
|
||||
let braceStack: number[] = [];
|
||||
let inBraces = false;
|
||||
let currentDepth = 0;
|
||||
const maxDepth = 3;
|
||||
|
||||
for (let i = 0; i < pattern.length; i++) {
|
||||
const char = pattern[i];
|
||||
const prevChar = i > 0 ? pattern[i - 1] : '';
|
||||
|
||||
// Check for opening brace
|
||||
if (char === '{' && prevChar !== '\\') {
|
||||
braceStack.push(i);
|
||||
currentDepth++;
|
||||
inBraces = true;
|
||||
|
||||
if (currentDepth > maxDepth) {
|
||||
errors.push({
|
||||
type: 'max_depth',
|
||||
position: i,
|
||||
message: `Nested too deep (max ${maxDepth} levels)`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for closing brace
|
||||
if (char === '}' && prevChar !== '\\') {
|
||||
if (braceStack.length === 0) {
|
||||
errors.push({
|
||||
type: 'unmatched_closing',
|
||||
position: i,
|
||||
message: 'Closing brace without opening brace'
|
||||
});
|
||||
} else {
|
||||
const openPos = braceStack.pop();
|
||||
const content = pattern.substring(openPos! + 1, i);
|
||||
|
||||
// Check for empty braces
|
||||
if (content.trim() === '') {
|
||||
errors.push({
|
||||
type: 'empty_braces',
|
||||
position: openPos!,
|
||||
message: 'Empty option set {}'
|
||||
});
|
||||
}
|
||||
|
||||
// Check for missing pipes
|
||||
if (!content.includes('|')) {
|
||||
errors.push({
|
||||
type: 'no_alternatives',
|
||||
position: openPos!,
|
||||
message: 'Option set must contain at least one pipe |'
|
||||
});
|
||||
}
|
||||
|
||||
// Check for empty options
|
||||
const options = content.split('|');
|
||||
for (let j = 0; j < options.length; j++) {
|
||||
if (options[j].trim() === '') {
|
||||
errors.push({
|
||||
type: 'empty_option',
|
||||
position: openPos! + content.indexOf('||'),
|
||||
message: 'Empty option between pipes'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
currentDepth--;
|
||||
}
|
||||
inBraces = braceStack.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unclosed braces
|
||||
if (braceStack.length > 0) {
|
||||
for (const pos of braceStack) {
|
||||
errors.push({
|
||||
type: 'unclosed_brace',
|
||||
position: pos,
|
||||
message: 'Opening brace not closed'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a few sample variations
|
||||
*/
|
||||
function generateSamples(pattern: string, count: number = 3): string[] {
|
||||
const samples: string[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let result = pattern;
|
||||
const regex = /\{([^{}]+)\}/g;
|
||||
|
||||
result = result.replace(regex, (match, content) => {
|
||||
const options = content.split('|').map((s: string) => s.trim());
|
||||
return options[Math.floor(Math.random() * options.length)];
|
||||
});
|
||||
|
||||
samples.push(result);
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
if (!validateGodToken(request)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const { pattern, max_depth = 3 } = await request.json();
|
||||
|
||||
if (!pattern) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Missing pattern'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const validation = validateSpintax(pattern);
|
||||
const samples = validation.valid ? generateSamples(pattern, 5) : [];
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
pattern,
|
||||
valid: validation.valid,
|
||||
errors: validation.errors,
|
||||
samples: validation.valid ? samples : null,
|
||||
stats: {
|
||||
length: pattern.length,
|
||||
braces: (pattern.match(/\{/g) || []).length,
|
||||
pipes: (pattern.match(/\|/g) || []).length
|
||||
},
|
||||
recommendation: validation.valid
|
||||
? '✅ Pattern is valid - safe to use in batch generation'
|
||||
: '❌ Fix errors before using in production',
|
||||
timestamp: new Date().toISOString()
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Validation failed',
|
||||
details: error.message
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
if (!validateGodToken(request)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
endpoint: 'POST /api/intelligence/spintax/validate',
|
||||
description: 'Validate spintax patterns before batch generation',
|
||||
examples: {
|
||||
valid: '{Hello|Hi|Hey} {world|there|friend}!',
|
||||
invalid: '{Hello|Hi} {world',
|
||||
nested: '{The {best|top} {solution|answer}}',
|
||||
empty: '{|option} // Error: empty option'
|
||||
},
|
||||
errors_detected: [
|
||||
'unmatched_braces',
|
||||
'empty_option_sets',
|
||||
'no_alternatives',
|
||||
'max_depth_exceeded'
|
||||
]
|
||||
}, null, 2), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user