Spaces:
Running
Running
File size: 14,098 Bytes
65ef364 e3c1646 65ef364 2271467 65ef364 c586a80 65ef364 c586a80 65ef364 c586a80 65ef364 c586a80 65ef364 c586a80 65ef364 e3c1646 65ef364 e3c1646 65ef364 e3c1646 65ef364 c586a80 65ef364 9500400 65ef364 e3c1646 72eb26e 65ef364 c586a80 65ef364 e3c1646 65ef364 c586a80 2271467 c586a80 2271467 c586a80 65ef364 e3c1646 c586a80 65ef364 2271467 65ef364 2271467 c89a5a0 2271467 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 |
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from "next/server";
import { GoogleGenAI } from "@google/genai";
import JSON5 from "json5";
import { InferenceClient } from "@huggingface/inference";
import type { AugmentRequest, AugmentResponse, FileUpdate } from "@/types";
const SYS = `You are Omni Engine, an autonomous code evolution system.
INPUTS:
- project context (tree + critical files)
- a user instruction
OUTPUT:
- STRICT JSON ONLY. No prose. No code fences. No leading or trailing text.
- Prefer this object shape:
{
"ok": true,
"summary": string,
"logs": string[],
"files": [ { "path": string, "action": "add"|"update"|"delete", "content"?: string, "note"?: string } ]
}
- If you cannot produce an object, return a top-level JSON array of the same file objects (files array).
RULES:
- Paths must be POSIX-like and rooted from repo (e.g., /frontend/src/App.tsx)
- For add/update include full file content (runnable)
- Keep changes consistent and compilable for the chosen language/framework
- DO NOT include markdown fences or explanation outside JSON
`;
function cleanText(raw: string): string {
let t = raw || "";
t = t.replace(/```[a-zA-Z]*\n?/g, "");
t = t.replace(/```/g, "");
return t.trim();
}
function tryParseAny(jsonish: string): any {
// Try strict JSON first
try {
return JSON.parse(jsonish);
} catch {
// Relaxed JSON5 parse (handles trailing commas, etc.)
try {
// Prefer full string
return JSON5.parse(jsonish);
} catch {
// Try to extract object
const sObj = jsonish.indexOf("{");
const eObj = jsonish.lastIndexOf("}");
if (sObj >= 0 && eObj > sObj) {
const candidate = jsonish.slice(sObj, eObj + 1);
try { return JSON.parse(candidate); } catch {}
try { return JSON5.parse(candidate); } catch {}
}
// Try to extract array
const sArr = jsonish.indexOf("[");
const eArr = jsonish.lastIndexOf("]");
if (sArr >= 0 && eArr > sArr) {
const candidate = jsonish.slice(sArr, eArr + 1);
try { return JSON.parse(candidate); } catch {}
try { return JSON5.parse(candidate); } catch {}
}
throw new Error("Model did not return valid JSON");
}
}
}
function findJsonCandidates(text: string, maxCandidates = 8): string[] {
const candidates: string[] = [];
const cleaned = cleanText(text);
// 1) Code-fenced JSON blocks (already stripped in cleanText, so skip)
// 2) Balanced scanning for objects and arrays
const openers = ['{', '['] as const;
for (let start = 0; start < cleaned.length; start++) {
const ch = cleaned[start];
if (!openers.includes(ch as any)) continue;
const isArray = ch === '[';
const closer = isArray ? ']' : '}';
let depth = 0;
let inStr: null | '"' | "'" = null;
let prev = '';
for (let i = start; i < cleaned.length; i++) {
const c = cleaned[i];
if (inStr) {
if (c === inStr && prev !== '\\') inStr = null as any;
} else {
if (c === '"' || c === "'") inStr = c as any;
else if (c === ch) depth++;
else if (c === closer) depth--;
if (depth === 0) {
const slice = cleaned.slice(start, i + 1).trim();
if ((isArray && slice.startsWith('[') && slice.endsWith(']')) || (!isArray && slice.startsWith('{') && slice.endsWith('}'))) {
candidates.push(slice);
if (candidates.length >= maxCandidates) return candidates;
}
break;
}
}
prev = c;
}
}
return candidates;
}
export async function POST(req: NextRequest) {
try {
const body = (await req.json()) as AugmentRequest;
const { context, instruction, language = "javascript", framework = "express-react", response_type = "file_updates", model, provider } = body || {} as AugmentRequest;
if (!context || !instruction) {
return NextResponse.json({ ok: false, message: "Missing context or instruction" } as AugmentResponse, { status: 400 });
}
const userPrompt = `Project Context:\n${context}\n\nInstruction:\n${instruction}\n\nTarget:\n- language: ${language}\n- framework: ${framework}\n- response_type: ${response_type}`;
let text = "";
if ((provider || "").toLowerCase() === "google" || (model || "").toLowerCase().startsWith("gemini-")) {
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) return NextResponse.json({ ok: false, message: "Missing GEMINI_API_KEY" } as AugmentResponse, { status: 500 });
const ai = new GoogleGenAI({ apiKey });
const res = await ai.models.generateContent({
model: model || "gemini-2.5-flash",
contents: [
{ role: "user", parts: [{ text: SYS }] },
{ role: "user", parts: [{ text: userPrompt }] },
],
config: { maxOutputTokens: 4096 },
} as any);
text = ((res as any)?.response?.text && (res as any).response.text())
|| (res as any)?.text
|| ((res as any)?.candidates?.[0]?.content?.parts?.map((p: any) => p?.text || "").join("") || "");
// Fallback: single user message with combined system + prompt
if (!text || !String(text).trim()) {
const res2 = await ai.models.generateContent({
model: model || "gemini-2.5-flash",
contents: [ { role: 'user', parts: [{ text: `${SYS}\n\n${userPrompt}` }] } ],
config: { maxOutputTokens: 4096 },
} as any);
text = ((res2 as any)?.response?.text && (res2 as any).response.text())
|| (res2 as any)?.text
|| ((res2 as any)?.candidates?.[0]?.content?.parts?.map((p: any) => p?.text || "").join("") || "");
}
} else {
const token = process.env.HF_TOKEN || process.env.DEFAULT_HF_TOKEN;
const client = new InferenceClient(token);
const res = await client.chatCompletion({
model: model || "deepseek-ai/DeepSeek-V3.1",
provider: (provider as any) || undefined,
messages: [
{ role: "system", content: SYS },
{ role: "user", content: userPrompt },
],
});
text = res.choices?.[0]?.message?.content || "";
}
const cleaned = cleanText(text);
if (!cleaned) {
const title = instruction.match(/Title:\s*(.*)/)?.[1] || "OmniDev App";
return NextResponse.json({ ok: true, files: generateFallbackFiles(framework, language, title) } as AugmentResponse, { status: 200 });
}
let json: any;
let parseErr: any = null;
try {
json = tryParseAny(cleaned);
} catch (e: any) {
parseErr = e;
// Try candidates extracted by balanced scanning
const cands = findJsonCandidates(cleaned, 12);
for (const c of cands) {
try { json = tryParseAny(c); parseErr = null; break; } catch {}
}
// Last resort: attempt to coerce by simple replacements
if (parseErr) {
try {
const coerced = cleaned
.replace(/(\w+)\s*:/g, '"$1":') // quote keys
.replace(/'([^']*)'/g, '"$1"'); // single to double quotes
json = tryParseAny(coerced);
parseErr = null;
} catch {}
}
// REPAIR PASS via Gemini: convert arbitrary text to strict JSON per schema
if (parseErr) {
try {
const apiKey = process.env.GEMINI_API_KEY;
if (apiKey) {
const ai = new GoogleGenAI({ apiKey });
const repairSys = `You are a JSON repair tool. Convert the user's content into STRICT JSON matching either: { ok, files[] } or just files[]. files[] is an array of { path, action, content?, note? }. No prose, no code fences.`;
const resR = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: [
{ role: 'user', parts: [{ text: repairSys }] },
{ role: 'user', parts: [{ text: cleaned }] },
],
config: { maxOutputTokens: 2048 },
} as any);
const repaired = ((resR as any)?.response?.text && (resR as any).response.text())
|| (resR as any)?.text
|| ((resR as any)?.candidates?.[0]?.content?.parts?.map((p: any) => p?.text || "").join("") || "");
const repairedClean = cleanText(repaired || "");
if (repairedClean) {
json = tryParseAny(repairedClean);
parseErr = null;
}
}
} catch {}
}
if (parseErr) {
// As a last-resort, generate a minimal runnable scaffold so the flow never breaks
const files = generateFallbackFiles(framework, language, (instruction.match(/Title:\s*(.*)/)?.[1] || "OmniDev App"));
return NextResponse.json({ ok: true, files } as AugmentResponse, { status: 200 });
}
}
// Minimal validation
if (json && json.ok && Array.isArray(json.files)) {
return NextResponse.json(json as AugmentResponse, { status: 200 });
}
// If model returned array at root, assume it's files
if (Array.isArray(json)) {
return NextResponse.json({ ok: true, files: json } as AugmentResponse, { status: 200 });
}
// If the object contains files nested under any key, lift it
if (json && typeof json === 'object') {
const stack: any[] = [json];
while (stack.length) {
const cur = stack.pop();
if (cur && typeof cur === 'object') {
if (Array.isArray(cur.files)) {
return NextResponse.json({ ok: true, files: cur.files, summary: cur.summary, logs: cur.logs } as AugmentResponse, { status: 200 });
}
for (const k of Object.keys(cur)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const v: any = (cur as any)[k];
if (v && typeof v === 'object') stack.push(v);
}
}
}
}
// Shape still unexpected: provide runnable fallback
const files = generateFallbackFiles(framework, language, (instruction.match(/Title:\s*(.*)/)?.[1] || "OmniDev App"));
return NextResponse.json({ ok: true, files } as AugmentResponse, { status: 200 });
} catch (e: any) {
return NextResponse.json({ ok: false, message: e?.message || "Internal error" } as AugmentResponse, { status: 500 });
}
}
function generateFallbackFiles(framework: string, language: string, title: string): FileUpdate[] {
const isTs = language.toLowerCase().includes('ts');
if (framework?.startsWith('express')) {
const server = `import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\napp.use(cors());\nconst distPath = path.join(__dirname, '..', 'frontend', 'dist');\napp.use(express.static(distPath));\napp.get('/health', (_, res) => res.json({ ok: true }));\napp.get('*', (req, res) => { try { res.sendFile(path.join(distPath, 'index.html')); } catch { res.send('OK'); } });\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => console.log('Server listening on', PORT));\n`;
const indexHtml = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n<title>${title}</title>\n<script src=\"https://cdn.tailwindcss.com\"></script>\n</head>\n<body class=\"bg-neutral-950 text-white\">\n<div id=\"root\"></div>\n<script type=\"module\" src=\"/src/main.${isTs ? 'tsx' : 'jsx'}\"></script>\n</body>\n</html>`;
const main = `import React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport App from './App';\ncreateRoot(document.getElementById('root')).render(<App />);\n`;
const app = `export default function App(){\n return (<main className=\"min-h-screen grid place-content-center\">\n <h1 className=\"text-3xl font-bold\">${title}</h1>\n <p className=\"text-neutral-400\">Frontend + Backend scaffolded by OmniDev</p>\n </main>);\n}\n`;
const fePkg = { name: "frontend", private: true, scripts: { dev: "vite", build: "vite build", preview: "vite preview" }, dependencies: { react: "^18.3.1", "react-dom": "^18.3.1" }, devDependencies: { vite: "^5.4.0" } };
const bePkg = { name: "backend", private: true, type: "module", scripts: { start: "node server.js" }, dependencies: { express: "^4.19.0", cors: "^2.8.5" } };
const files: FileUpdate[] = [
{ path: "/backend/server.js", action: "add", content: server },
{ path: "/backend/package.json", action: "add", content: JSON.stringify(bePkg, null, 2) },
{ path: "/frontend/index.html", action: "add", content: indexHtml },
{ path: `/frontend/src/main.${isTs ? 'tsx' : 'jsx'}`, action: "add", content: main },
{ path: `/frontend/src/App.${isTs ? 'tsx' : 'jsx'}`, action: "add", content: app },
{ path: "/frontend/package.json", action: "add", content: JSON.stringify(fePkg, null, 2) },
{ path: "/README.md", action: "add", content: `# ${title}\n\nGenerated by OmniDev scaffold.` },
];
return files;
}
if (framework?.startsWith('next')) {
const page = `export default function Page(){ return (<main style={{minHeight:'100vh',display:'grid',placeContent:'center'}}><h1>${title}</h1><p>Next.js scaffold by OmniDev</p></main>); }`;
const api = `export async function GET(){ return Response.json({ ok: true }); }`;
const pkg = { name: "next-app", private: true, scripts: { dev: "next dev", build: "next build", start: "next start" }, dependencies: { next: "^15.0.0", react: "^18.3.1", "react-dom": "^18.3.1" } };
return [
{ path: "/app/page.tsx", action: "add", content: page },
{ path: "/app/api/health/route.ts", action: "add", content: api },
{ path: "/package.json", action: "add", content: JSON.stringify(pkg, null, 2) },
{ path: "/README.md", action: "add", content: `# ${title}\n\nGenerated by OmniDev scaffold.` },
];
}
return [ { path: "/README.md", action: "add", content: `# ${title}\n\nOmniDev scaffold.` } ];
}
|