Spaces:
Running
Running
| 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}`); | |
| }); |