Bluestrikeai commited on
Commit
b724e07
Β·
verified Β·
1 Parent(s): 03535ab

Create pipeline.py

Browse files
Files changed (1) hide show
  1. pipeline.py +439 -0
pipeline.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+ import asyncio
4
+ import traceback
5
+
6
+ from agents import ResearchAgent, OrchestratorAgent, FrontendAgent, BackendAgent
7
+ from schemas import ProjectState, AgentMessage
8
+
9
+
10
+ class PipelineEngine:
11
+ def __init__(self):
12
+ self.research = ResearchAgent()
13
+ self.orchestrator = OrchestratorAgent()
14
+ self.frontend = FrontendAgent()
15
+ self.backend = BackendAgent()
16
+
17
+ # ── helpers ───────────────────────────────────────────
18
+ def _emit(
19
+ self,
20
+ state: ProjectState,
21
+ event_type: str,
22
+ agent: str,
23
+ content: str = "",
24
+ file_path: str | None = None,
25
+ metadata: dict | None = None,
26
+ ):
27
+ state.messages.append(
28
+ AgentMessage(
29
+ event_type=event_type,
30
+ agent=agent,
31
+ content=content,
32
+ file_path=file_path,
33
+ metadata=metadata or {},
34
+ )
35
+ )
36
+
37
+ # ── main pipeline ────────────────────────────────────
38
+ async def run(self, state: ProjectState):
39
+ try:
40
+ # ── 1. Research ──────────────────────────────
41
+ state.status = "researching"
42
+ state.current_agent = "research"
43
+ self._emit(state, "agent_start", "research", "πŸ” Starting research…")
44
+
45
+ research = await self._do_research(state)
46
+ state.research_output = research
47
+ self._emit(
48
+ state,
49
+ "agent_done",
50
+ "research",
51
+ "βœ… Research complete",
52
+ metadata={"keys": list(research.keys())},
53
+ )
54
+
55
+ # ── 2. Orchestrate ───────────────────────────
56
+ state.status = "orchestrating"
57
+ state.current_agent = "orchestrator"
58
+ self._emit(state, "agent_start", "orchestrator", "🧠 Building blueprint…")
59
+
60
+ blueprint = await self._do_orchestrate(state)
61
+ state.blueprint = blueprint
62
+ self._emit(
63
+ state,
64
+ "agent_done",
65
+ "orchestrator",
66
+ "βœ… Blueprint ready",
67
+ metadata={"systems": list(blueprint.get("systems", {}).keys())},
68
+ )
69
+
70
+ # ── 3 & 4. Frontend + Backend in parallel ────
71
+ state.status = "building"
72
+ state.current_agent = "frontend+backend"
73
+ self._emit(state, "agent_start", "system", "πŸš€ Generating code…")
74
+
75
+ await asyncio.gather(
76
+ self._do_frontend(state),
77
+ self._do_backend(state),
78
+ )
79
+
80
+ # ── 5. Merge ────────────────────────────────
81
+ state.status = "merging"
82
+ state.current_agent = "system"
83
+ self._emit(state, "agent_start", "system", "πŸ“¦ Merging…")
84
+
85
+ state.file_tree = sorted(state.generated_files.keys())
86
+ self._create_preview(state)
87
+
88
+ state.status = "completed"
89
+ state.current_agent = ""
90
+ self._emit(
91
+ state,
92
+ "agent_done",
93
+ "system",
94
+ f"πŸŽ‰ Done β€” {len(state.generated_files)} files generated",
95
+ )
96
+
97
+ except Exception as exc:
98
+ state.status = "error"
99
+ state.errors.append(str(exc))
100
+ self._emit(
101
+ state,
102
+ "error",
103
+ state.current_agent or "system",
104
+ f"❌ {exc}",
105
+ metadata={"tb": traceback.format_exc()},
106
+ )
107
+
108
+ # ── research phase ───────────────────────────────────
109
+ async def _do_research(self, state: ProjectState) -> dict:
110
+ prompt = (
111
+ f"Research this app idea and produce a JSON report:\n\n"
112
+ f"APP IDEA: {state.user_prompt}\n"
113
+ f"APP TYPE: {state.app_type}\n"
114
+ f"SYSTEMS: {', '.join(state.systems)}\n\n"
115
+ "Return JSON with keys: stack_recommendation, schema_hints, "
116
+ "api_docs_summary, security_notes, hosting_notes, ui_patterns, "
117
+ "competitor_analysis"
118
+ )
119
+ buf: list[str] = []
120
+ async for tok in self.research.call([{"role": "user", "content": prompt}]):
121
+ buf.append(tok)
122
+ if len(buf) % 30 == 0:
123
+ self._emit(state, "token", "research", tok)
124
+ text = "".join(buf)
125
+ self._emit(state, "token", "research", "\n[done]")
126
+ return self._parse_json(text)
127
+
128
+ # ── orchestrate phase ────────────────────────────────
129
+ async def _do_orchestrate(self, state: ProjectState) -> dict:
130
+ prompt = (
131
+ f"Create a master blueprint for:\n\n"
132
+ f"USER REQUEST: {state.user_prompt}\n"
133
+ f"APP TYPE: {state.app_type}\n"
134
+ f"SYSTEMS: {', '.join(state.systems)}\n\n"
135
+ f"RESEARCH:\n{json.dumps(state.research_output, indent=2, default=str)[:6000]}\n\n"
136
+ "Produce a complete master blueprint JSON following your instructions."
137
+ )
138
+ buf: list[str] = []
139
+ async for tok in self.orchestrator.call([{"role": "user", "content": prompt}]):
140
+ buf.append(tok)
141
+ if len(buf) % 20 == 0:
142
+ self._emit(state, "token", "orchestrator", tok)
143
+ text = "".join(buf)
144
+ return self._parse_json(text)
145
+
146
+ # ── frontend phase ───────────────────────────────────
147
+ async def _do_frontend(self, state: ProjectState):
148
+ self._emit(state, "agent_start", "frontend", "🎨 Generating frontend…")
149
+
150
+ for system in state.systems:
151
+ sys_bp = state.blueprint.get("systems", {}).get(system, {})
152
+ design = state.blueprint.get("design_tokens", {})
153
+ pay = state.blueprint.get("payment_config", {})
154
+
155
+ prompt = (
156
+ f'Generate complete frontend for the "{system}" system.\n\n'
157
+ f"BLUEPRINT:\n{json.dumps(sys_bp, indent=2, default=str)}\n\n"
158
+ f"DESIGN TOKENS:\n{json.dumps(design, indent=2, default=str)}\n\n"
159
+ f"PAYMENT CONFIG:\n{json.dumps(pay, indent=2, default=str)}\n\n"
160
+ f"DB SCHEMA (ref):\n"
161
+ f"{json.dumps(state.blueprint.get('database_schema',{}), indent=2, default=str)[:3000]}\n\n"
162
+ f"Mark every file: // FILE: {system}/path/file.jsx\n"
163
+ "Generate ALL files. Use React + Tailwind."
164
+ )
165
+ buf: list[str] = []
166
+ async for tok in self.frontend.call([{"role": "user", "content": prompt}]):
167
+ buf.append(tok)
168
+ if len(buf) % 15 == 0:
169
+ self._emit(state, "token", "frontend", tok)
170
+ text = "".join(buf)
171
+ for path, content in self._extract_files(text).items():
172
+ state.generated_files[path] = content
173
+ self._emit(state, "file_created", "frontend", f"πŸ“„ {path}", file_path=path)
174
+
175
+ self._emit(state, "agent_done", "frontend", "βœ… Frontend complete")
176
+
177
+ # ── backend phase ────────────────────────────────────
178
+ async def _do_backend(self, state: ProjectState):
179
+ self._emit(state, "agent_start", "backend", "πŸ” Generating backend…")
180
+
181
+ prompt = (
182
+ f"Generate complete backend for this app.\n\n"
183
+ f"DB SCHEMA:\n{json.dumps(state.blueprint.get('database_schema',{}), indent=2, default=str)}\n\n"
184
+ f"API ENDPOINTS:\n{json.dumps(state.blueprint.get('api_endpoints',[]), indent=2, default=str)}\n\n"
185
+ f"AUTH CONFIG:\n{json.dumps(state.blueprint.get('auth_config',{}), indent=2, default=str)}\n\n"
186
+ f"PAYMENT CONFIG:\n{json.dumps(state.blueprint.get('payment_config',{}), indent=2, default=str)}\n\n"
187
+ f"APP: {state.user_prompt}\n\n"
188
+ "Generate: SQL schema, FastAPI backend, Edge Functions, Docker files, "
189
+ ".env.example, firebase.json, DEPLOY.md.\n"
190
+ "Mark files: # FILE: path or -- FILE: path"
191
+ )
192
+ buf: list[str] = []
193
+ async for tok in self.backend.call([{"role": "user", "content": prompt}]):
194
+ buf.append(tok)
195
+ if len(buf) % 15 == 0:
196
+ self._emit(state, "token", "backend", tok)
197
+ text = "".join(buf)
198
+ for path, content in self._extract_files(text).items():
199
+ state.generated_files[path] = content
200
+ self._emit(state, "file_created", "backend", f"πŸ“„ {path}", file_path=path)
201
+
202
+ self._emit(state, "agent_done", "backend", "βœ… Backend complete")
203
+
204
+ # ── bug fix ──────────────────────────────────────────
205
+ async def fix(self, state: ProjectState, error_msg: str, file_path: str | None):
206
+ self._emit(state, "agent_start", "backend", f"πŸ”§ Fixing: {error_msg[:80]}")
207
+
208
+ code = ""
209
+ if file_path and file_path in state.generated_files:
210
+ code = state.generated_files[file_path][:8000]
211
+
212
+ prompt = (
213
+ f"FIX THIS BUG:\nERROR: {error_msg}\n"
214
+ + (f"FILE: {file_path}\nCODE:\n{code}\n" if code else "")
215
+ + "\nOutput the COMPLETE fixed file with original file path marker."
216
+ )
217
+
218
+ is_fe = file_path and any(
219
+ file_path.endswith(e) for e in (".jsx", ".tsx", ".css", ".html", ".js")
220
+ )
221
+ agent = self.frontend if is_fe else self.backend
222
+ name = "frontend" if is_fe else "backend"
223
+
224
+ buf: list[str] = []
225
+ async for tok in agent.call([{"role": "user", "content": prompt}]):
226
+ buf.append(tok)
227
+ text = "".join(buf)
228
+ for p, c in self._extract_files(text).items():
229
+ state.generated_files[p] = c
230
+ self._emit(state, "file_created", name, f"πŸ”§ Fixed {p}", file_path=p)
231
+
232
+ state.status = "completed"
233
+ self._emit(state, "agent_done", name, "βœ… Fix applied")
234
+
235
+ # ── JSON parser ──────────────────────────────────────
236
+ def _parse_json(self, text: str) -> dict:
237
+ for pattern in [
238
+ r"```json\s*([\s\S]*?)\s*```",
239
+ r"```\s*([\s\S]*?)\s*```",
240
+ ]:
241
+ for m in re.findall(pattern, text):
242
+ try:
243
+ return json.loads(m)
244
+ except json.JSONDecodeError:
245
+ continue
246
+ # try raw
247
+ brace = text.find("{")
248
+ if brace != -1:
249
+ depth, end = 0, brace
250
+ for i in range(brace, len(text)):
251
+ if text[i] == "{":
252
+ depth += 1
253
+ elif text[i] == "}":
254
+ depth -= 1
255
+ if depth == 0:
256
+ end = i + 1
257
+ break
258
+ try:
259
+ return json.loads(text[brace:end])
260
+ except json.JSONDecodeError:
261
+ pass
262
+
263
+ return self._fallback_blueprint()
264
+
265
+ def _fallback_blueprint(self) -> dict:
266
+ return {
267
+ "project_name": "generated-app",
268
+ "description": "AI-generated web application",
269
+ "systems": {
270
+ "client_portal": {
271
+ "pages": [
272
+ {"path": "/dashboard", "title": "Dashboard", "components": ["Layout", "Stats", "Activity"], "auth_required": True},
273
+ {"path": "/settings", "title": "Settings", "components": ["ProfileForm", "PasswordForm"], "auth_required": True},
274
+ {"path": "/billing", "title": "Billing", "components": ["PlanCards", "PayPalButton", "InvoiceList"], "auth_required": True},
275
+ ],
276
+ "features": ["auth", "dashboard", "billing", "settings"],
277
+ },
278
+ "public_landing": {
279
+ "pages": [
280
+ {"path": "/", "title": "Home", "components": ["Hero", "Features", "Pricing", "CTA", "Footer"], "auth_required": False},
281
+ {"path": "/pricing", "title": "Pricing", "components": ["PricingCards", "FAQ"], "auth_required": False},
282
+ ],
283
+ "features": ["hero", "pricing", "signup", "seo"],
284
+ },
285
+ "marketing_cms": {
286
+ "pages": [
287
+ {"path": "/blog", "title": "Blog", "components": ["BlogList", "PostEditor"], "auth_required": True},
288
+ ],
289
+ "features": ["blog", "email_capture"],
290
+ },
291
+ "analytics_dashboard": {
292
+ "pages": [
293
+ {"path": "/analytics", "title": "Analytics", "components": ["Charts", "Metrics", "UserTable"], "auth_required": True},
294
+ ],
295
+ "features": ["realtime_metrics", "charts", "revenue"],
296
+ },
297
+ "admin_panel": {
298
+ "pages": [
299
+ {"path": "/admin", "title": "Admin", "components": ["UserMgmt", "Logs", "SystemHealth"], "auth_required": True},
300
+ ],
301
+ "features": ["user_management", "moderation", "logs"],
302
+ },
303
+ },
304
+ "database_schema": {"tables": []},
305
+ "api_endpoints": [],
306
+ "auth_config": {"providers": ["email"]},
307
+ "payment_config": {
308
+ "provider": "paypal",
309
+ "plans": [
310
+ {"name": "Free", "price": 0, "features": ["Basic"]},
311
+ {"name": "Pro", "price": 29, "features": ["All features"]},
312
+ ],
313
+ },
314
+ "design_tokens": {
315
+ "colors": {
316
+ "primary": "#6C63FF",
317
+ "secondary": "#00D9FF",
318
+ "background": "#0A0A0F",
319
+ "surface": "#111118",
320
+ "text": "#F0F0FF",
321
+ },
322
+ "fonts": {"heading": "Inter", "body": "Inter", "mono": "JetBrains Mono"},
323
+ },
324
+ }
325
+
326
+ # ── file extractor ───────────────────────────────────
327
+ def _extract_files(self, text: str) -> dict[str, str]:
328
+ files: dict[str, str] = {}
329
+ pattern = r"(?://|#|--)\s*FILE:\s*(.+?)(?:\n)([\s\S]*?)(?=(?://|#|--)\s*FILE:|$)"
330
+ for path, content in re.findall(pattern, text):
331
+ clean = path.strip()
332
+ body = re.sub(r"\s*```\s*$", "", content.strip())
333
+ body = re.sub(r"^```\w*\s*", "", body.strip())
334
+ if clean and body:
335
+ files[clean] = body
336
+
337
+ if not files:
338
+ blocks = re.findall(
339
+ r"(?:#+\s*)?(?:`([^`]+)`|(\S+\.\w+))\s*\n```\w*\n([\s\S]*?)```", text
340
+ )
341
+ for n1, n2, body in blocks:
342
+ name = (n1 or n2 or "").strip()
343
+ if name and body.strip():
344
+ files[name] = body.strip()
345
+
346
+ if not files:
347
+ if "def " in text or "import " in text:
348
+ files["backend/generated.py"] = text
349
+ elif "CREATE TABLE" in text.upper():
350
+ files["database/schema.sql"] = text
351
+ elif "function" in text or "const " in text:
352
+ files["frontend/generated.jsx"] = text
353
+ else:
354
+ files["output/raw.txt"] = text
355
+
356
+ return files
357
+
358
+ # ── preview builder ──────────────────────────────────
359
+ def _create_preview(self, state: ProjectState):
360
+ bp = state.blueprint
361
+ colors = bp.get("design_tokens", {}).get("colors", {})
362
+ name = bp.get("project_name", "Generated App")
363
+ systems = bp.get("systems", {})
364
+
365
+ cards = ""
366
+ for sname, sdata in systems.items():
367
+ pages = sdata.get("pages", [])
368
+ feats = sdata.get("features", [])
369
+ nice = sname.replace("_", " ").title()
370
+ feat_html = "".join(f'<span class="t">{f}</span>' for f in feats[:6])
371
+ page_html = "".join(
372
+ f'<li>{p.get("title", p.get("path", ""))}</li>' for p in pages[:6]
373
+ )
374
+ cards += (
375
+ f'<div class="c"><h3>{nice}</h3>'
376
+ f'<div class="ts">{feat_html}</div>'
377
+ f"<ul>{page_html}</ul>"
378
+ f'<small>{len(pages)} pages</small></div>'
379
+ )
380
+
381
+ fc = len(state.generated_files)
382
+ ext_counts: dict[str, int] = {}
383
+ for f in state.generated_files:
384
+ e = f.rsplit(".", 1)[-1] if "." in f else "other"
385
+ ext_counts[e] = ext_counts.get(e, 0) + 1
386
+ fstats = " Β· ".join(f"{v} .{k}" for k, v in sorted(ext_counts.items()))
387
+ total_pages = sum(len(s.get("pages", [])) for s in systems.values())
388
+
389
+ pri = colors.get("primary", "#6C63FF")
390
+ sec = colors.get("secondary", "#00D9FF")
391
+ bg = colors.get("background", "#0A0A0F")
392
+ sf = colors.get("surface", "#111118")
393
+ tx = colors.get("text", "#F0F0FF")
394
+
395
+ html = f"""<!DOCTYPE html>
396
+ <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
397
+ <title>{name}</title>
398
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=JetBrains+Mono&display=swap" rel="stylesheet">
399
+ <style>
400
+ *{{margin:0;padding:0;box-sizing:border-box}}
401
+ body{{font-family:'Inter',sans-serif;background:{bg};color:{tx};min-height:100vh;padding:2rem}}
402
+ .w{{max-width:1100px;margin:0 auto}}
403
+ .hero{{text-align:center;padding:3.5rem 2rem;background:linear-gradient(135deg,{sf},{bg});border-radius:20px;border:1px solid #1E1E2E;margin-bottom:2rem}}
404
+ .hero h1{{font-size:2.6rem;font-weight:700;background:linear-gradient(135deg,{pri},{sec});-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:.8rem}}
405
+ .hero p{{color:#8888AA;font-size:1.1rem;max-width:550px;margin:0 auto}}
406
+ .stats{{display:flex;gap:1rem;justify-content:center;margin-top:1.5rem;flex-wrap:wrap}}
407
+ .s{{background:{sf};border:1px solid #1E1E2E;border-radius:12px;padding:.8rem 1.3rem;text-align:center}}
408
+ .sv{{font-size:1.8rem;font-weight:700;color:{pri}}}
409
+ .sl{{color:#8888AA;font-size:.8rem;margin-top:.15rem}}
410
+ .g{{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1.2rem;margin-top:1.5rem}}
411
+ .c{{background:{sf};border:1px solid #1E1E2E;border-radius:14px;padding:1.3rem;transition:.3s}}
412
+ .c:hover{{border-color:{pri}44;transform:translateY(-2px);box-shadow:0 6px 24px {pri}15}}
413
+ .c h3{{font-size:1.1rem;font-weight:600;margin-bottom:.6rem}}
414
+ .ts{{display:flex;flex-wrap:wrap;gap:.3rem;margin-bottom:.8rem}}
415
+ .t{{background:{pri}22;color:{pri};padding:.15rem .5rem;border-radius:5px;font-size:.7rem;font-weight:500}}
416
+ ul{{list-style:none;margin-bottom:.5rem}}
417
+ li{{padding:.25rem 0;color:#8888AA;font-size:.85rem;border-bottom:1px solid #1E1E2E}}
418
+ li:last-child{{border:none}}
419
+ small{{color:{sec};font-size:.75rem}}
420
+ .fi{{text-align:center;margin-top:1.5rem;color:#8888AA;font-family:'JetBrains Mono',monospace;font-size:.8rem}}
421
+ .sec{{font-size:1.3rem;font-weight:600;margin-bottom:.4rem}}
422
+ @keyframes fadeIn{{from{{opacity:0;transform:translateY(8px)}}to{{opacity:1;transform:translateY(0)}}}}
423
+ .c{{animation:fadeIn .5s ease forwards}}
424
+ .c:nth-child(2){{animation-delay:.1s}}.c:nth-child(3){{animation-delay:.2s}}
425
+ .c:nth-child(4){{animation-delay:.3s}}.c:nth-child(5){{animation-delay:.4s}}
426
+ </style></head>
427
+ <body><div class="w">
428
+ <div class="hero"><h1>{name}</h1><p>{state.user_prompt[:180]}</p>
429
+ <div class="stats">
430
+ <div class="s"><div class="sv">{len(systems)}</div><div class="sl">Systems</div></div>
431
+ <div class="s"><div class="sv">{fc}</div><div class="sl">Files</div></div>
432
+ <div class="s"><div class="sv">{total_pages}</div><div class="sl">Pages</div></div>
433
+ </div></div>
434
+ <h2 class="sec">Generated Systems</h2>
435
+ <div class="g">{cards}</div>
436
+ <div class="fi">{fstats}</div>
437
+ </div></body></html>"""
438
+
439
+ state.generated_files["preview/index.html"] = html