ai_plugin_server / app2.js
everydaycats's picture
Rename app.js to app2.js
77a0e4b verified
import express from 'express';
import bodyParser from 'body-parser';
import cors from 'cors';
import { StateManager } from './stateManager.js';
import { AIEngine } from './aiEngine.js';
import fs from 'fs';
import admin from 'firebase-admin';
// --- FIREBASE SETUP ---
// Using environment variable injection as requested
let db = null;
try {
if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON && process.env.FIREBASE_SERVICE_ACCOUNT_JSON !== "") {
const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
// Make sure to replace this URL or add it to env variables as well
databaseURL: "https://shago-web-default-rtdb.firebaseio.com"
});
db = admin.database();
console.log("🔥 Firebase Connected");
} else {
console.warn("⚠️ No FIREBASE_SERVICE_ACCOUNT_JSON found. Running in Memory-Only mode.");
}
} catch (e) {
console.error("Firebase Init Error:", e);
}
const app = express();
const PORT = process.env.PORT || 7860;
// Load Prompts for Server-Side Logic Checks
const sysPrompts = JSON.parse(fs.readFileSync('./prompts.json', 'utf8'));
app.use(cors());
app.use(bodyParser.json({ limit: '50mb' }));
// --- HELPERS ---
const validateRequest = (req, res, next) => {
if (req.path.includes('/onboarding')) return next();
const { userId, projectId } = req.body;
if (!userId || !projectId) return res.status(400).json({ error: "Missing ID" });
next();
};
function extractWorkerPrompt(text) {
const match = text.match(/WORKER_PROMPT:\s*(.*)/s);
return match ? match[1].trim() : null;
}
function formatContext({ hierarchyContext, scriptContext, logContext }) {
let out = "";
if (scriptContext) out += `\n[TARGET SCRIPT]: ${scriptContext.targetName}\n[SOURCE PREVIEW]: ${scriptContext.scriptSource?.substring(0, 1000)}...`;
if (logContext) out += `\n[LAST LOGS]: ${logContext.logs}`;
return out;
}
function extractPMQuestion(text) {
const match = text.match(/\[ASK_PM:\s*(.*?)\]/s);
return match ? match[1].trim() : null;
}
function extractImagePrompt(text) {
const match = text.match(/\[GENERATE_IMAGE:\s*(.*?)\]/s);
return match ? match[1].trim() : null;
}
// --- ONBOARDING ENDPOINTS ---
/**
* 1. ANALYZE
*/
app.post('/onboarding/analyze', validateRequest, async (req, res) => {
const { description } = req.body;
if (!description) return res.status(400).json({ error: "Description required" });
try {
console.log(`[Onboarding] Analyzing idea...`);
const result = await AIEngine.generateEntryQuestions(description);
if (result.status === "REJECTED") {
return res.json({
rejected: true,
reason: result.reason || "Idea violates TOS or guidelines."
});
}
res.json({ questions: result.questions });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Analysis failed" });
}
});
/**
* 2. CREATE
*/
app.post('/onboarding/create', validateRequest, async (req, res) => {
const { userId, description, answers } = req.body;
const projectId = "proj_" + Date.now();
try {
console.log(`[Onboarding] Grading Project ${projectId}...`);
// STEP 1: GRADE
const grading = await AIEngine.gradeProject(description, answers);
// STEP 2: CHECK FAIL CONDITIONS (Relaxed)
// Only fail if Feasibility is extremely low (< 30) or Rating is F
const isFailure = grading.feasibility < 30 || grading.rating === 'F';
let thumbnailBase64 = null;
if (isFailure) {
console.log(`[Onboarding] ❌ Project Failed Grading (${grading.rating}). Skipping Image.`);
} else {
console.log(`[Onboarding] ✅ Passed. Generating Thumbnail...`);
// --- CRITICAL FIX HERE ---
// We MUST send the description so the AI knows what to draw.
const imagePrompt = `Game Title: ${grading.title}. Core Concept: ${description}`;
thumbnailBase64 = await AIEngine.generateImage(imagePrompt);
}
const projectData = {
id: projectId,
userId,
title: grading.title || "Untitled Project",
description,
answers,
stats: grading,
thumbnail: thumbnailBase64, // ? `data:image/png;base64,${thumbnailBase64}` : null,
createdAt: Date.now(),
status: isFailure ? "rejected" : "initialized"
};
if (db) await db.ref(`projects/${projectId}`).set(projectData);
if (!isFailure) {
await StateManager.updateProject(projectId, {
...projectData,
workerHistory: [],
pmHistory: [],
failureCount: 0
});
}
res.json({
success: !isFailure,
projectId,
stats: grading,
title: projectData.title,
thumbnail: projectData.thumbnail
});
} catch (err) {
console.error("Create Error:", err);
res.status(500).json({ error: "Creation failed" });
}
});
// --- CORE WORKFLOW ENDPOINTS ---
/**
* 3. INITIALIZE WORKSPACE (PM Generates GDD -> First Task)
*/
app.post('/new/project', validateRequest, async (req, res) => {
const { userId, projectId, description } = req.body;
try {
const pmHistory = [];
const gddPrompt = `Create a comprehensive GDD for: ${description}`;
const gddResponse = await AIEngine.callPM(pmHistory, gddPrompt);
pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
pmHistory.push({ role: 'model', parts: [{ text: gddResponse }] });
const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
const taskResponse = await AIEngine.callPM(pmHistory, taskPrompt);
pmHistory.push({ role: 'user', parts: [{ text: taskPrompt }] });
pmHistory.push({ role: 'model', parts: [{ text: taskResponse }] });
const initialWorkerInstruction = extractWorkerPrompt(taskResponse) || `Initialize structure for: ${description}`;
const workerHistory = [];
const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
const workerResponse = await AIEngine.callWorker(workerHistory, initialWorkerPrompt, []);
workerHistory.push({ role: 'user', parts: [{ text: initialWorkerPrompt }] });
workerHistory.push({ role: 'model', parts: [{ text: workerResponse }] });
// Update State
await StateManager.updateProject(projectId, {
userId,
pmHistory,
workerHistory,
gdd: gddResponse,
failureCount: 0
});
// Queue Actions
await processAndQueueResponse(projectId, workerResponse);
res.json({ success: true, message: "Workspace Initialized", gddPreview: gddResponse.substring(0, 200) });
} catch (err) {
console.error("Init Error:", err);
res.status(500).json({ error: "Initialization Failed" });
}
});
/**
* 4. FEEDBACK LOOP (The "Brain")
*/
app.post('/project/feedback', async (req, res) => {
const { projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete, images } = req.body;
const project = await StateManager.getProject(projectId);
if (!project) return res.status(404).json({ error: "Project not found." });
// A. TASK COMPLETE -> SCOPE SWITCH
if (taskComplete) {
console.log(`[${projectId}] ✅ TASK COMPLETE.`);
const summary = `Worker completed the previous task. Logs: ${logContext?.logs || "Clean"}. \nGenerate the NEXT task using 'WORKER_PROMPT:' format.`;
const pmResponse = await AIEngine.callPM(project.pmHistory, summary);
project.pmHistory.push({ role: 'user', parts: [{ text: summary }] });
project.pmHistory.push({ role: 'model', parts: [{ text: pmResponse }] });
const nextInstruction = extractWorkerPrompt(pmResponse);
if (!nextInstruction) {
await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, status: "IDLE" });
return res.json({ success: true, message: "No further tasks. Project Idle." });
}
console.log(`[${projectId}] 🧹 Nuking Worker for next task.`);
const newWorkerHistory = [];
const newPrompt = `New Objective: ${nextInstruction}`;
const workerResponse = await AIEngine.callWorker(newWorkerHistory, newPrompt, []);
newWorkerHistory.push({ role: 'user', parts: [{ text: newPrompt }] });
newWorkerHistory.push({ role: 'model', parts: [{ text: workerResponse }] });
await StateManager.updateProject(projectId, {
pmHistory: project.pmHistory,
workerHistory: newWorkerHistory,
failureCount: 0
});
StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "warn('Starting Next Task...')" });
await processAndQueueResponse(projectId, workerResponse);
return res.json({ success: true, message: "Next Task Assigned" });
}
// B. ERROR ESCALATION
let isFailure = false;
if (logContext?.logs) {
const errs = ["Error", "Exception", "failed", "Stack Begin", "Infinite yield"];
if (errs.some(k => logContext.logs.includes(k))) {
isFailure = true;
project.failureCount = (project.failureCount || 0) + 1;
}
}
if (project.failureCount > 3) {
console.log(`[${projectId}] 🚨 Escalating to PM...`);
const pmPrompt = sysPrompts.pm_guidance_prompt.replace('{{LOGS}}', logContext?.logs);
const pmVerdict = await AIEngine.callPM(project.pmHistory, pmPrompt);
if (pmVerdict.includes("[TERMINATE]")) {
const fixInstruction = pmVerdict.replace("[TERMINATE]", "").trim();
const resetHistory = [];
const resetPrompt = `[SYSTEM]: Previous worker terminated. \nNew Objective: ${fixInstruction}`;
const workerResp = await AIEngine.callWorker(resetHistory, resetPrompt, []);
resetHistory.push({ role: 'user', parts: [{ text: resetPrompt }] });
resetHistory.push({ role: 'model', parts: [{ text: workerResp }] });
await StateManager.updateProject(projectId, { workerHistory: resetHistory, failureCount: 0 });
StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "print('SYSTEM: Worker Reset')" });
await processAndQueueResponse(projectId, workerResp);
return res.json({ success: true, message: "Worker Terminated." });
} else {
const injection = `[PM GUIDANCE]: ${pmVerdict} \n\nApply this fix now.`;
const workerResp = await AIEngine.callWorker(project.workerHistory, injection, []);
project.workerHistory.push({ role: 'user', parts: [{ text: injection }] });
project.workerHistory.push({ role: 'model', parts: [{ text: workerResp }] });
await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, failureCount: 1 });
await processAndQueueResponse(projectId, workerResp);
return res.json({ success: true, message: "PM Guidance Applied." });
}
}
// C. STANDARD LOOP + CONSULTATION
try {
const fullInput = `USER: ${prompt || "Automatic Feedback"}` + formatContext({ hierarchyContext, scriptContext, logContext });
let response = await AIEngine.callWorker(project.workerHistory, fullInput, images || []);
// CHECK: Consultant Question
const pmQuestion = extractPMQuestion(response);
if (pmQuestion) {
console.log(`[${projectId}] 🙋 Worker asking PM: "${pmQuestion}"`);
const pmConsultPrompt = `[WORKER CONSULTATION]: The Worker asks: "${pmQuestion}"\nProvide a technical answer to unblock them.`;
const pmAnswer = await AIEngine.callPM(project.pmHistory, pmConsultPrompt);
// Update PM Context
project.pmHistory.push({ role: 'user', parts: [{ text: pmConsultPrompt }] });
project.pmHistory.push({ role: 'model', parts: [{ text: pmAnswer }] });
// Feed Answer Back
const injectionMsg = `[PM RESPONSE]: ${pmAnswer}`;
project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
// Resume Worker
response = await AIEngine.callWorker(project.workerHistory, injectionMsg, []);
project.workerHistory.push({ role: 'user', parts: [{ text: injectionMsg }] });
project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
} else {
project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
}
await StateManager.updateProject(projectId, {
workerHistory: project.workerHistory,
pmHistory: project.pmHistory,
failureCount: project.failureCount
});
await processAndQueueResponse(projectId, response);
res.json({ success: true });
} catch (err) {
console.error("AI Error:", err);
res.status(500).json({ error: "AI Failed" });
}
});
/**
* 5. PING (Plugin Polling)
*/
app.post('/project/ping', async (req, res) => {
const { projectId } = req.body;
const command = await StateManager.popCommand(projectId);
if (command) {
if (command.payload === "CLEAR_CONSOLE") {
res.json({ action: "CLEAR_LOGS" });
} else {
res.json({
action: command.type,
target: command.payload,
code: command.type === 'EXECUTE' ? command.payload : null
});
}
} else {
res.json({ action: "IDLE" });
}
});
/**
* 6. MANUAL OVERRIDE
*/
app.post('/human/override', validateRequest, async (req, res) => {
const { projectId, instruction, pruneHistory } = req.body;
const project = await StateManager.getProject(projectId);
const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
if (pruneHistory && project.workerHistory.length >= 2) {
project.workerHistory.pop();
project.workerHistory.pop();
}
const response = await AIEngine.callWorker(project.workerHistory, overrideMsg, []);
project.workerHistory.push({ role: 'user', parts: [{ text: overrideMsg }] });
project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
await StateManager.updateProject(projectId, { workerHistory: project.workerHistory });
await processAndQueueResponse(projectId, response);
res.json({ success: true });
});
// --- RESPONSE PROCESSOR ---
async function processAndQueueResponse(projectId, rawResponse) {
// 1. Intercept Image Requests
const imgPrompt = extractImagePrompt(rawResponse);
if (imgPrompt) {
console.log(`[${projectId}] 🎨 Generating Asset: ${imgPrompt}`);
const base64Image = await AIEngine.generateImage(imgPrompt);
if (base64Image) {
// Queue asset creation for Plugin
await StateManager.queueCommand(projectId, {
type: "CREATE_ASSET",
payload: base64Image
});
}
}
// 2. Queue the Raw Response (StateManager parses code)
await StateManager.queueCommand(projectId, rawResponse);
}
app.listen(PORT, () => {
console.log(`AI Backend Running on ${PORT}`);
});