rasAli02 commited on
Commit
1035089
·
1 Parent(s): b0a3339

🐛 Fix: Resolve backend 500 on Vercel by implementing FastAPI and syncing dependencies

Browse files
backend/agents.py CHANGED
@@ -185,14 +185,13 @@ async def _call_amd_vllm(
185
  "temperature": 0.1, # Low temperature for deterministic structured output
186
  }
187
 
188
- # Candidate endpoints
189
  base_url = AMD_INFERENCE_URL.rstrip("/")
 
 
 
 
190
  candidates = [
191
- f"{base_url}/proxy/8000/v1/chat/completions",
192
- f"{base_url}/proxy/8001/v1/chat/completions",
193
- f"{base_url}:8000/v1/chat/completions",
194
- f"{base_url}:8001/v1/chat/completions",
195
- f"{base_url}/v1/chat/completions",
196
  ]
197
 
198
  headers = {}
@@ -297,6 +296,16 @@ async def run_pipeline(
297
  ),
298
  )
299
 
 
 
 
 
 
 
 
 
 
 
300
  model_label = AMD_MODEL_NAME
301
  return {
302
  "agents": [
@@ -304,6 +313,7 @@ async def run_pipeline(
304
  {"role": "diagnostician", "label": "Diagnostician Agent", "model": model_label, "output": diagnostician},
305
  {"role": "action", "label": "Action Agent", "model": model_label, "output": action},
306
  {"role": "reporter", "label": "Reporter Agent", "model": model_label, "output": reporter},
 
307
  ],
308
  }
309
 
 
185
  "temperature": 0.1, # Low temperature for deterministic structured output
186
  }
187
 
 
188
  base_url = AMD_INFERENCE_URL.rstrip("/")
189
+ if not base_url.startswith("http"):
190
+ base_url = f"http://{base_url}"
191
+ if "/proxy/8000" not in base_url:
192
+ base_url = f"{base_url}/proxy/8000"
193
  candidates = [
194
+ f"{base_url}/v1/chat/completions"
 
 
 
 
195
  ]
196
 
197
  headers = {}
 
296
  ),
297
  )
298
 
299
+ # 5) Social (text only)
300
+ social = await _run_agent(
301
+ "social",
302
+ SOCIAL_SYSTEM,
303
+ (
304
+ f"INSPECTOR_REPORT:\n{json.dumps(inspector['parsed'])}\n\n"
305
+ f"REPORTER_SUMMARY:\n{json.dumps(reporter['parsed'])}"
306
+ ),
307
+ )
308
+
309
  model_label = AMD_MODEL_NAME
310
  return {
311
  "agents": [
 
313
  {"role": "diagnostician", "label": "Diagnostician Agent", "model": model_label, "output": diagnostician},
314
  {"role": "action", "label": "Action Agent", "model": model_label, "output": action},
315
  {"role": "reporter", "label": "Reporter Agent", "model": model_label, "output": reporter},
316
+ {"role": "social", "label": "Social Agent", "model": model_label, "output": social},
317
  ],
318
  }
319
 
backend/app.py CHANGED
@@ -1,20 +1,22 @@
1
- """
2
- ForgeSight — Hugging Face Spaces Gradio backend.
3
- Wraps the multi-agent pipeline so the React frontend can call it
4
- via the Gradio Client JS SDK or plain HTTP POST to /api/<fn_name>.
5
-
6
- Deploy: push this repo to a HF Space (Gradio SDK).
7
- """
8
  import os
9
- import json
10
- import math
11
- import time
12
  import uuid
13
- import gradio as gr
 
 
 
 
 
14
  from datetime import datetime, timezone
 
 
 
 
 
 
 
15
 
16
- # ── Import the agent pipeline ───────────────────────────────────────────────
17
- from agents import run_pipeline, generate_social_post
18
 
19
  # ── MONGODB PERSISTENCE (optional, falls back to in-memory) ──────────────────
20
  MONGO_URL = os.getenv("MONGO_URL", "")
@@ -30,10 +32,17 @@ async def _init_db():
30
  """Attempt to connect to MongoDB; silently fall back to in-memory if unavailable."""
31
  global _db, _inspections_col, _journal_col
32
  if not MONGO_URL:
 
33
  return
34
  try:
35
  from motor.motor_asyncio import AsyncIOMotorClient
36
- client = AsyncIOMotorClient(MONGO_URL, serverSelectionTimeoutMS=4000)
 
 
 
 
 
 
37
  await client.admin.command("ping")
38
  _db = client["forgesight"]
39
  _inspections_col = _db["inspections"]
@@ -66,52 +75,154 @@ async def _db_list_journal(limit=50) -> list:
66
  return await cursor.to_list(length=limit)
67
  return _mem_journal[:limit]
68
 
 
69
 
70
  def _now_iso() -> str:
71
  return datetime.now(timezone.utc).isoformat()
72
 
 
 
 
 
 
73
 
74
- # ── 1. Inspection endpoint ──────────────────────────────────────────────────
75
- async def inspect(image_base64: str, notes: str = "", product_spec: str = "", source: str = "upload"):
76
- """Run the 4-agent inspection pipeline on a base64 image."""
77
- # Strip potential data-URI prefix
78
- if "," in image_base64 and image_base64.strip().startswith("data:"):
79
- image_base64 = image_base64.split(",", 1)[1]
80
 
81
- transcript = await run_pipeline(
82
- image_base64=image_base64,
83
- notes=notes or "",
84
- product_spec=product_spec or "",
85
- )
 
 
 
 
 
 
86
 
87
- inspection = {
88
- "id": str(uuid.uuid4()),
89
- "created_at": _now_iso(),
90
- "notes": notes or "",
91
- "product_spec": product_spec or "",
92
- "source": source or "upload",
93
- "transcript": transcript,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  }
95
- await _db_insert_inspection(inspection)
96
 
97
- summary = _summarize(inspection)
98
- return json.dumps({
99
- "id": inspection["id"],
100
- "created_at": inspection["created_at"],
101
- "transcript": transcript,
102
- "summary": summary,
103
- })
104
 
 
105
 
106
- # ── 2. List inspections ─────────────────────────────────────────────────────
107
- async def list_inspections(limit: int = 50):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  docs = await _db_list_inspections(limit)
109
  items = [_summarize(doc) for doc in docs]
110
- return json.dumps({"items": items, "total": len(items)})
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- # ── 3. Metrics ───────────────────────────────────────────────────────────────
114
- async def metrics():
115
  docs = await _db_list_inspections(500)
116
  total = len(docs)
117
  verdict_counts = {"pass": 0, "warn": 0, "fail": 0}
@@ -134,107 +245,50 @@ async def metrics():
134
 
135
  avg_conf = sum(confidences) / len(confidences) if confidences else 0.0
136
  top_defects = sorted(defect_type_counts.items(), key=lambda x: x[1], reverse=True)[:6]
137
- quality_score = 0
138
- if total > 0:
139
- quality_score = round(100 * (verdict_counts["pass"] + 0.5 * verdict_counts["warn"]) / total)
140
 
141
- return json.dumps({
142
  "total_inspections": total,
143
  "verdict_counts": verdict_counts,
144
  "avg_confidence": round(avg_conf, 3),
145
  "top_defects": [{"type": t, "count": c} for t, c in top_defects],
146
  "quality_score": quality_score,
147
- })
148
 
 
 
 
149
 
150
- # ── 4. Telemetry (simulated MI300X) ─────────────────────────────────────────
151
- async def telemetry():
152
- t = time.time()
153
- gpu_util = 62 + 30 * math.sin(t / 4.0)
154
- vram_used = 88 + 20 * math.sin(t / 7.0)
155
- tokens_per_sec = 2850 + 450 * math.sin(t / 3.0)
156
- power_w = 620 + 80 * math.sin(t / 5.0)
157
- temp_c = 58 + 7 * math.sin(t / 6.0)
158
- return json.dumps({
159
- "simulated": True,
160
- "device": "AMD Instinct MI300X",
161
- "gpu_util_pct": round(max(0, min(100, gpu_util)), 1),
162
- "vram_used_gb": round(max(0, vram_used), 1),
163
- "vram_total_gb": 192.0,
164
- "tokens_per_sec": int(max(0, tokens_per_sec)),
165
- "power_watts": int(max(0, power_w)),
166
- "temp_c": round(max(0, temp_c), 1),
167
- "ts": _now_iso(),
168
- })
169
-
170
-
171
- # ── 5. Blueprint ────────────────────────────────────────────────────────────
172
- async def blueprint():
173
- return json.dumps({
174
  "stack": [
175
- {
176
- "layer": "Hardware",
177
- "title": "AMD Instinct MI300X",
178
- "detail": "192 GB HBM3 · 5.3 TB/s memory bandwidth · GPU node",
179
- "why": "Massive VRAM enables serving 70B-class Qwen-VL models without sharding.",
180
- },
181
- {
182
- "layer": "Runtime",
183
- "title": "ROCm 6.2",
184
- "detail": "Open compute runtime · HIP · MIOpen · RCCL",
185
- "why": "PyTorch + vLLM run natively on MI300X via ROCm.",
186
- },
187
- {
188
- "layer": "Serving",
189
- "title": "vLLM on ROCm",
190
- "detail": "PagedAttention · continuous batching · OpenAI-compatible API",
191
- "why": "High-throughput multimodal inference for the agent pipeline.",
192
- },
193
- {
194
- "layer": "Model",
195
- "title": "Qwen2-VL-72B (fine-tuned)",
196
- "detail": "LoRA fine-tune on defect-image + work-order pairs via Optimum-AMD",
197
- "why": "Domain-specialized vision reasoning beats zero-shot generic VLMs.",
198
- },
199
- {
200
- "layer": "Agents",
201
- "title": "Inspector → Diagnostician → Action → Reporter",
202
- "detail": "Sequential multi-agent with structured JSON hand-offs",
203
- "why": "Interpretable, auditable pipeline for industrial QC.",
204
- },
205
- {
206
- "layer": "Product",
207
- "title": "ForgeSight Console",
208
- "detail": "React + FastAPI · live transcript · defect feed · build journal",
209
- "why": "End-to-end demonstrable app shipped for the hackathon.",
210
- },
211
- ],
212
- "finetune_recipe": {
213
- "base_model": "Qwen/Qwen2-VL-72B-Instruct",
214
- "dataset": "ForgeSight-QC-10K (proprietary defect-image ↔ work-order pairs)",
215
- "method": "QLoRA r=64 · Optimum-AMD · bf16",
216
- "hardware": "1× MI300X node (8 GPUs)",
217
- "expected_wall_clock": "~6h for 3 epochs on 10K pairs",
218
- "serve_with": "vLLM 0.6+ on ROCm",
219
- },
220
- })
221
-
222
 
223
- # ── 6. Journal ──────────────────────────────────────────────────────────────
224
- async def journal_list():
225
- docs = await _db_list_journal(50)
226
- # Auto-seed if empty
227
- if not docs:
228
  await _seed_journal()
229
- docs = await _db_list_journal(50)
230
- return json.dumps({"items": docs, "total": len(docs)})
231
-
232
-
233
- async def journal_create(title: str, body: str, tags: str = ""):
234
- tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else []
 
 
 
 
235
  try:
236
  social = await generate_social_post(title, body)
237
- except Exception:
238
  social = {"x_post": "", "linkedin_post": ""}
239
 
240
  entry = {
@@ -242,246 +296,28 @@ async def journal_create(title: str, body: str, tags: str = ""):
242
  "created_at": _now_iso(),
243
  "title": title,
244
  "body": body,
245
- "tags": tag_list,
246
  "x_post": social.get("x_post", ""),
247
  "linkedin_post": social.get("linkedin_post", ""),
248
  }
249
  await _db_insert_journal(entry)
250
- return json.dumps(entry)
251
 
 
252
 
253
- async def _seed_journal():
254
- existing = await _db_list_journal(1)
255
- if existing:
256
- return
257
- seeds = [
258
- {
259
- "title": "Kickoff: ForgeSight on AMD Developer Cloud",
260
- "body": "Spun up an MI300X instance on AMD Developer Cloud. First impression: zero CUDA-lock-in, ROCm + PyTorch just worked. Targeting all three hackathon tracks with one agentic multimodal QC copilot.",
261
- "tags": ["kickoff", "amd", "rocm"],
262
- },
263
- {
264
- "title": "Multi-agent pipeline wired end-to-end",
265
- "body": "Inspector → Diagnostician → Action → Reporter. Each agent produces strict JSON so hand-offs stay auditable. Running on Claude Sonnet 4.5 today, swapping to Qwen2-VL on MI300X next.",
266
- "tags": ["agents", "pipeline", "qwen"],
267
- },
268
- {
269
- "title": "Fine-tune recipe: QLoRA on Qwen2-VL with Optimum-AMD",
270
- "body": "Drafted the LoRA fine-tune path for 10K defect-image ↔ work-order pairs. Expecting ~6h wall-clock on a single MI300X node. vLLM-ROCm will serve the result.",
271
- "tags": ["fine-tuning", "qlora", "optimum-amd"],
272
- },
273
- ]
274
- for s in seeds:
275
- try:
276
- social = await generate_social_post(s["title"], s["body"])
277
- except Exception:
278
- social = {"x_post": "", "linkedin_post": ""}
279
- entry = {
280
- "id": str(uuid.uuid4()),
281
- "created_at": _now_iso(),
282
- **s,
283
- "x_post": social.get("x_post", ""),
284
- "linkedin_post": social.get("linkedin_post", ""),
285
- }
286
- await _db_insert_journal(entry)
287
 
 
 
 
 
 
 
288
 
289
- # ── Helpers ──────────────────────────────────────────────────────────────────
290
- def _summarize(inspection: dict) -> dict:
291
- agents = inspection.get("transcript", {}).get("agents", [])
292
- inspector = next((a for a in agents if a["role"] == "inspector"), None)
293
- reporter = next((a for a in agents if a["role"] == "reporter"), None)
294
- action = next((a for a in agents if a["role"] == "action"), None)
295
-
296
- inspector_out = (inspector or {}).get("output", {}).get("parsed", {}) or {}
297
- reporter_out = (reporter or {}).get("output", {}).get("parsed", {}) or {}
298
- action_out = (action or {}).get("output", {}).get("parsed", {}) or {}
299
-
300
- defects = inspector_out.get("defects") or []
301
- return {
302
- "id": inspection["id"],
303
- "created_at": inspection["created_at"],
304
- "verdict": inspector_out.get("verdict", "warn"),
305
- "confidence": float(inspector_out.get("confidence", 0.0) or 0.0),
306
- "headline": reporter_out.get("headline") or inspector_out.get("observation", "Inspection complete")[:60],
307
- "defect_count": len(defects) if isinstance(defects, list) else 0,
308
- "priority": action_out.get("priority", "P2"),
309
- "source": inspection.get("source", "upload"),
310
- }
311
-
312
-
313
- # ── Health / root check ─────────────────────────────────────────────────────
314
- async def health():
315
- return json.dumps({
316
- "service": "forgesight",
317
- "status": "online",
318
- "track": "AMD Hackathon — Tracks 1+2+3",
319
- "runtime": "Hugging Face Spaces (Gradio)",
320
- })
321
-
322
-
323
- # ── Build the Gradio app ────────────────────────────────────────────────────
324
- # Each gr.Interface becomes a named API endpoint at /api/<fn_name>
325
- # The React frontend calls these via fetch() to the HF Space URL.
326
-
327
- with gr.Blocks(title="ForgeSight — AMD MI300X QC Copilot") as demo:
328
- gr.Markdown("# 🔍 ForgeSight — Multimodal QC Copilot")
329
- gr.Markdown("Backend API for the ForgeSight React frontend. Powered by AMD Instinct MI300X + ROCm.")
330
-
331
- # --- API-only endpoints (hidden UI, exposed as /api/...) ---
332
-
333
- # Health check
334
- health_btn = gr.Button("Health Check", visible=False)
335
- health_out = gr.Textbox(visible=False)
336
- health_btn.click(fn=health, inputs=[], outputs=health_out, api_name="health")
337
-
338
- # Inspect
339
- inspect_img = gr.Textbox(visible=False)
340
- inspect_notes = gr.Textbox(visible=False)
341
- inspect_spec = gr.Textbox(visible=False)
342
- inspect_source = gr.Textbox(visible=False)
343
- inspect_out = gr.Textbox(visible=False)
344
- inspect_btn = gr.Button("Inspect", visible=False)
345
- inspect_btn.click(
346
- fn=inspect,
347
- inputs=[inspect_img, inspect_notes, inspect_spec, inspect_source],
348
- outputs=inspect_out,
349
- api_name="inspect",
350
- )
351
-
352
- # List inspections
353
- list_limit = gr.Number(visible=False, value=50)
354
- list_out = gr.Textbox(visible=False)
355
- list_btn = gr.Button("List", visible=False)
356
- list_btn.click(fn=list_inspections, inputs=[list_limit], outputs=list_out, api_name="list_inspections")
357
-
358
- # Metrics
359
- metrics_out = gr.Textbox(visible=False)
360
- metrics_btn = gr.Button("Metrics", visible=False)
361
- metrics_btn.click(fn=metrics, inputs=[], outputs=metrics_out, api_name="metrics")
362
-
363
- # Telemetry
364
- telem_out = gr.Textbox(visible=False)
365
- telem_btn = gr.Button("Telemetry", visible=False)
366
- telem_btn.click(fn=telemetry, inputs=[], outputs=telem_out, api_name="telemetry")
367
-
368
- # Blueprint
369
- bp_out = gr.Textbox(visible=False)
370
- bp_btn = gr.Button("Blueprint", visible=False)
371
- bp_btn.click(fn=blueprint, inputs=[], outputs=bp_out, api_name="blueprint")
372
-
373
- # Journal list
374
- jl_out = gr.Textbox(visible=False)
375
- jl_btn = gr.Button("Journal List", visible=False)
376
- jl_btn.click(fn=journal_list, inputs=[], outputs=jl_out, api_name="journal_list")
377
-
378
- # Journal create
379
- jc_title = gr.Textbox(visible=False)
380
- jc_body = gr.Textbox(visible=False)
381
- jc_tags = gr.Textbox(visible=False)
382
- jc_out = gr.Textbox(visible=False)
383
- jc_btn = gr.Button("Journal Create", visible=False)
384
- jc_btn.click(
385
- fn=journal_create,
386
- inputs=[jc_title, jc_body, jc_tags],
387
- outputs=jc_out,
388
- api_name="journal_create",
389
- )
390
-
391
- # --- Visible demo UI for HF Space visitors ---
392
- with gr.Tab("🔬 Quick Inspect"):
393
- gr.Markdown("Upload an image to run the 4-agent QC pipeline.")
394
- with gr.Row():
395
- with gr.Column():
396
- demo_img = gr.Image(type="filepath", label="Product Image")
397
- demo_notes = gr.Textbox(label="Operator Notes", placeholder="e.g. batch B-124, shift 2")
398
- demo_spec = gr.Textbox(label="Product Spec", placeholder="e.g. aluminum 6061 bracket")
399
- demo_run = gr.Button("🚀 Run Inspection", variant="primary")
400
- with gr.Column():
401
- demo_result = gr.JSON(label="Pipeline Result")
402
-
403
- async def demo_inspect(img_path, notes, spec):
404
- if not img_path:
405
- return {"error": "Please upload an image"}
406
- import base64
407
- with open(img_path, "rb") as f:
408
- b64 = base64.b64encode(f.read()).decode()
409
- raw = await inspect(b64, notes or "", spec or "", "upload")
410
- return json.loads(raw)
411
-
412
- demo_run.click(fn=demo_inspect, inputs=[demo_img, demo_notes, demo_spec], outputs=demo_result)
413
-
414
- with gr.Tab("📊 Status"):
415
- gr.Markdown("### Service Status")
416
- status_btn = gr.Button("Check Status")
417
- status_out = gr.JSON()
418
- async def check_status():
419
- h = json.loads(await health())
420
- m = json.loads(await metrics())
421
- return {**h, **m}
422
- status_btn.click(fn=check_status, inputs=[], outputs=status_out)
423
-
424
- with gr.Tab("📐 Architecture"):
425
- gr.Markdown("### ForgeSight Agentic Pipeline Architecture")
426
- gr.HTML("""
427
- <div style="background: #0d0d10; padding: 20px; border: 1px solid #333; border-radius: 8px; font-family: sans-serif;">
428
- <svg viewBox="0 0 800 400" xmlns="http://www.w3.org/2000/svg">
429
- <!-- Data Flow -->
430
- <rect x="50" y="150" width="120" height="60" rx="4" fill="#141416" stroke="#333" />
431
- <text x="110" y="185" text-anchor="middle" fill="white" font-size="14">Image Upload</text>
432
-
433
- <path d="M 170 180 L 220 180" stroke="#ED1C24" stroke-width="2" marker-end="url(#arrow)" />
434
-
435
- <rect x="220" y="150" width="120" height="60" rx="4" fill="#ED1C24" stroke="#ED1C24" />
436
- <text x="280" y="185" text-anchor="middle" fill="white" font-size="14" font-weight="bold">vLLM / MI300X</text>
437
-
438
- <path d="M 340 180 L 390 180" stroke="#ED1C24" stroke-width="2" marker-end="url(#arrow)" />
439
-
440
- <!-- Agents -->
441
- <rect x="390" y="50" width="100" height="40" rx="4" fill="#141416" stroke="#ED1C24" />
442
- <text x="440" y="75" text-anchor="middle" fill="white" font-size="12">Inspector</text>
443
-
444
- <rect x="390" y="120" width="100" height="40" rx="4" fill="#141416" stroke="#ED1C24" />
445
- <text x="440" y="145" text-anchor="middle" fill="white" font-size="12">Diagnostician</text>
446
-
447
- <rect x="390" y="190" width="100" height="40" rx="4" fill="#141416" stroke="#ED1C24" />
448
- <text x="440" y="215" text-anchor="middle" fill="white" font-size="12">Action</text>
449
-
450
- <rect x="390" y="260" width="100" height="40" rx="4" fill="#141416" stroke="#ED1C24" />
451
- <text x="440" y="285" text-anchor="middle" fill="white" font-size="12">Reporter</text>
452
-
453
- <!-- Connections -->
454
- <path d="M 440 90 L 440 120" stroke="#666" stroke-width="1" />
455
- <path d="M 440 160 L 440 190" stroke="#666" stroke-width="1" />
456
- <path d="M 440 230 L 440 260" stroke="#666" stroke-width="1" />
457
-
458
- <path d="M 490 155 L 550 155" stroke="#ED1C24" stroke-width="2" marker-end="url(#arrow)" />
459
-
460
- <rect x="550" y="130" width="150" height="100" rx="4" fill="#141416" stroke="#333" />
461
- <text x="625" y="165" text-anchor="middle" fill="white" font-size="14">MongoDB Archival</text>
462
- <text x="625" y="190" text-anchor="middle" fill="#666" font-size="12">Persistence Layer</text>
463
-
464
- <defs>
465
- <marker id="arrow" markerWidth="10" markerHeight="10" refX="0" refY="3" orient="auto" markerUnits="strokeWidth">
466
- <path d="M0,0 L0,6 L9,3 z" fill="#ED1C24" />
467
- </marker>
468
- </defs>
469
- </svg>
470
- </div>
471
- """)
472
- gr.Markdown("""
473
- ### Stack Details
474
- - **Hardware**: AMD Instinct MI300X (192GB VRAM)
475
- - **Runtime**: ROCm 6.2 + PyTorch
476
- - **Inference**: vLLM (OpenAI-compatible)
477
- - **Persistence**: MongoDB Atlas
478
- """)
479
-
480
 
481
  if __name__ == "__main__":
482
- import asyncio
483
- # Initialize DB before launching
484
- loop = asyncio.get_event_loop()
485
- loop.run_until_complete(_init_db())
486
-
487
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
 
1
  import os
 
 
 
2
  import uuid
3
+ import time
4
+ import math
5
+ import httpx
6
+ import json
7
+ import tempfile
8
+ import asyncio
9
  from datetime import datetime, timezone
10
+ from typing import List, Optional
11
+
12
+ import gradio as gr
13
+ from fastapi import FastAPI, Request
14
+ from fastapi.responses import JSONResponse, FileResponse
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from fpdf import FPDF
17
 
18
+ # Import our agent pipeline
19
+ from agents import run_pipeline, AMD_INFERENCE_URL, AMD_MODEL_NAME, AMD_INFERENCE_TOKEN, generate_social_post
20
 
21
  # ── MONGODB PERSISTENCE (optional, falls back to in-memory) ──────────────────
22
  MONGO_URL = os.getenv("MONGO_URL", "")
 
32
  """Attempt to connect to MongoDB; silently fall back to in-memory if unavailable."""
33
  global _db, _inspections_col, _journal_col
34
  if not MONGO_URL:
35
+ print("⚠️ MONGO_URL not set – using in-memory storage")
36
  return
37
  try:
38
  from motor.motor_asyncio import AsyncIOMotorClient
39
+ import certifi
40
+ client = AsyncIOMotorClient(
41
+ MONGO_URL,
42
+ serverSelectionTimeoutMS=5000,
43
+ tlsCAFile=certifi.where()
44
+ )
45
+ # Verify connection
46
  await client.admin.command("ping")
47
  _db = client["forgesight"]
48
  _inspections_col = _db["inspections"]
 
75
  return await cursor.to_list(length=limit)
76
  return _mem_journal[:limit]
77
 
78
+ # ── HELPERS ───────────────────────────────────────────────────────────────────
79
 
80
  def _now_iso() -> str:
81
  return datetime.now(timezone.utc).isoformat()
82
 
83
+ def _summarize(inspection: dict) -> dict:
84
+ agents = inspection.get("transcript", {}).get("agents", [])
85
+ inspector = next((a for a in agents if a["role"] == "inspector"), None)
86
+ reporter = next((a for a in agents if a["role"] == "reporter"), None)
87
+ action = next((a for a in agents if a["role"] == "action"), None)
88
 
89
+ inspector_out = (inspector or {}).get("output", {}).get("parsed", {}) or {}
90
+ reporter_out = (reporter or {}).get("output", {}).get("parsed", {}) or {}
91
+ action_out = (action or {}).get("output", {}).get("parsed", {}) or {}
 
 
 
92
 
93
+ defects = inspector_out.get("defects") or []
94
+ return {
95
+ "id": inspection["id"],
96
+ "created_at": inspection["created_at"],
97
+ "verdict": inspector_out.get("verdict", "warn"),
98
+ "confidence": float(inspector_out.get("confidence", 0.0) or 0.0),
99
+ "headline": (reporter_out.get("headline") or inspector_out.get("observation", "Inspection complete"))[:60],
100
+ "defect_count": len(defects) if isinstance(defects, list) else 0,
101
+ "priority": action_out.get("priority", "P2"),
102
+ "source": inspection.get("source", "upload"),
103
+ }
104
 
105
+ async def _seed_journal():
106
+ """Seed the journal with initial milestones (instant, no LLM calls)."""
107
+ existing = await _db_list_journal(1)
108
+ if existing:
109
+ return
110
+ seeds = [
111
+ {
112
+ "title": "Kickoff: ForgeSight on AMD Developer Cloud",
113
+ "body": "Spun up an MI300X instance on AMD Developer Cloud. First impression: zero CUDA-lock-in, ROCm + PyTorch just worked.",
114
+ "tags": ["kickoff", "amd", "rocm"],
115
+ "x_post": "🚀 ForgeSight is live! We've officially spun up an AMD Instinct MI300X instance on the Developer Cloud. Zero CUDA-lock-in, just raw ROCm power. #AMDHackathon #ROCm #AIatAMD @lablab @AIatAMD",
116
+ "linkedin_post": "We've officially kicked off ForgeSight for the AMD + lablab.ai Hackathon! We're leveraging the massive 192GB VRAM of the MI300X to build a production-ready QC pipeline. #AI #AMD #Engineering",
117
+ },
118
+ {
119
+ "title": "Multi-agent pipeline wired end-to-end",
120
+ "body": "Inspector → Diagnostician → Action → Reporter. Each agent produces strict JSON so hand-offs stay auditable.",
121
+ "tags": ["agents", "pipeline", "qwen"],
122
+ "x_post": "Our 4-agent pipeline is wired! Inspector → Diagnostician → Action → Reporter. Real-time vision reasoning on MI300X. #AIatAMD #AMDHackathon @lablab",
123
+ "linkedin_post": "Auditability is key in industrial QC. ForgeSight's multi-agent pipeline ensures every decision is grounded in structured data. #QualityControl #Agents",
124
+ },
125
+ ]
126
+ for s in seeds:
127
+ entry = {
128
+ "id": str(uuid.uuid4()),
129
+ "created_at": _now_iso(),
130
+ **s,
131
+ }
132
+ await _db_insert_journal(entry)
133
+
134
+ # ── API LOGIC ─────────────────────────────────────────────────────────────────
135
+
136
+ async def api_get_telemetry():
137
+ t = time.time()
138
+ status = "Connected"
139
+ error_msg = None
140
+
141
+ # FOR HACKATHON DEMO: Simulated data for premium UI visuals
142
+ gpu_util = 65 + 25 * math.sin(t / 4.0)
143
+ vram_used = 142.0 + 10 * math.sin(t / 6.0)
144
+ tokens_per_sec = int(2700 + 300 * math.sin(t / 3.0))
145
+ power_w = int(480 + 50 * math.sin(t / 5.0))
146
+
147
+ return {
148
+ "gpu_util_pct": round(gpu_util, 1),
149
+ "vram_used_gb": round(vram_used, 1),
150
+ "vram_total_gb": 192.0,
151
+ "temp_c": round(64 + 4 * math.sin(t / 7.0), 1),
152
+ "power_watts": power_w,
153
+ "tokens_per_sec": tokens_per_sec,
154
+ "device": "AMD Instinct MI300X",
155
+ "status": status,
156
+ "is_simulated": True,
157
+ "persistence": "MongoDB" if _inspections_col is not None else "In-Memory",
158
+ "ts": _now_iso(),
159
  }
 
160
 
161
+ # ── FASTAPI SETUP ─────────────────────────────────────────────────────────────
 
 
 
 
 
 
162
 
163
+ app = FastAPI(title="ForgeSight API")
164
 
165
+ app.add_middleware(
166
+ CORSMiddleware,
167
+ allow_origins=["*"],
168
+ allow_methods=["*"],
169
+ allow_headers=["*"],
170
+ )
171
+
172
+ @app.on_event("startup")
173
+ async def startup_event():
174
+ await _init_db()
175
+ await _seed_journal()
176
+
177
+ @app.get("/api")
178
+ @app.get("/api/health")
179
+ async def handle_health():
180
+ return {"status": "online", "service": "forgesight", "db": "connected" if _inspections_col is not None else "memory"}
181
+
182
+ @app.get("/api/inspections")
183
+ async def get_inspections(limit: int = 50):
184
  docs = await _db_list_inspections(limit)
185
  items = [_summarize(doc) for doc in docs]
186
+ return {"items": items, "total": len(items)}
187
+
188
+ @app.post("/api/inspections")
189
+ async def create_inspection(request: Request):
190
+ data = await request.json()
191
+ image_base64 = data.get("image_base64", "")
192
+ notes = data.get("notes", "")
193
+ product_spec = data.get("product_spec", "")
194
+ source = data.get("source", "upload")
195
+
196
+ if image_base64 and "," in image_base64:
197
+ image_base64 = image_base64.split(",")[1]
198
+
199
+ transcript = await run_pipeline(image_base64, notes, product_spec)
200
 
201
+ inspection = {
202
+ "id": str(uuid.uuid4()),
203
+ "created_at": _now_iso(),
204
+ "notes": notes or "",
205
+ "product_spec": product_spec or "",
206
+ "source": source or "upload",
207
+ "transcript": transcript,
208
+ }
209
+ await _db_insert_inspection(inspection)
210
+ return inspection
211
+
212
+ @app.get("/api/inspections/{inspection_id}")
213
+ async def get_inspection(inspection_id: str):
214
+ inspection = None
215
+ if _inspections_col is not None:
216
+ inspection = await _inspections_col.find_one({"id": inspection_id}, {"_id": 0})
217
+ else:
218
+ inspection = next((i for i in _mem_inspections if i["id"] == inspection_id), None)
219
+
220
+ if not inspection:
221
+ return JSONResponse({"detail": "Inspection not found"}, status_code=404)
222
+ return inspection
223
 
224
+ @app.get("/api/metrics")
225
+ async def get_metrics():
226
  docs = await _db_list_inspections(500)
227
  total = len(docs)
228
  verdict_counts = {"pass": 0, "warn": 0, "fail": 0}
 
245
 
246
  avg_conf = sum(confidences) / len(confidences) if confidences else 0.0
247
  top_defects = sorted(defect_type_counts.items(), key=lambda x: x[1], reverse=True)[:6]
248
+ quality_score = round(100 * (verdict_counts["pass"] + 0.5 * verdict_counts["warn"]) / total) if total > 0 else 100
 
 
249
 
250
+ return {
251
  "total_inspections": total,
252
  "verdict_counts": verdict_counts,
253
  "avg_confidence": round(avg_conf, 3),
254
  "top_defects": [{"type": t, "count": c} for t, c in top_defects],
255
  "quality_score": quality_score,
256
+ }
257
 
258
+ @app.get("/api/telemetry")
259
+ async def get_telemetry():
260
+ return await api_get_telemetry()
261
 
262
+ @app.get("/api/blueprint")
263
+ async def get_blueprint():
264
+ return {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  "stack": [
266
+ {"layer": "Hardware", "title": "AMD Instinct MI300X", "detail": "192 GB HBM3 · 5.3 TB/s bandwidth", "why": "Enables massive VRAM pools for multimodal Qwen-VL."},
267
+ {"layer": "Runtime", "title": "ROCm 6.2", "detail": "Open compute stack · PyTorch 2.4", "why": "Native AMD acceleration without CUDA lock-in."},
268
+ {"layer": "Serving", "title": "vLLM", "detail": "PagedAttention · continuous batching", "why": "High-throughput serving for agentic chains."},
269
+ {"layer": "Model", "title": "Qwen2-VL-72B", "detail": "Fine-tuned for structural defects", "why": "Domain-specialized vision reasoning."},
270
+ {"layer": "Agents", "title": "Sequential Agentic Chain", "detail": "Structured JSON hand-offs", "why": "Auditability and reliability."},
271
+ ]
272
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
 
274
+ @app.get("/api/journal")
275
+ async def list_journal():
276
+ items = await _db_list_journal(50)
277
+ if not items:
 
278
  await _seed_journal()
279
+ items = await _db_list_journal(50)
280
+ return {"items": items, "total": len(items)}
281
+
282
+ @app.post("/api/journal")
283
+ async def create_journal(request: Request):
284
+ data = await request.json()
285
+ title = data.get("title", "")
286
+ body = data.get("body", "")
287
+ tags = data.get("tags", [])
288
+
289
  try:
290
  social = await generate_social_post(title, body)
291
+ except:
292
  social = {"x_post": "", "linkedin_post": ""}
293
 
294
  entry = {
 
296
  "created_at": _now_iso(),
297
  "title": title,
298
  "body": body,
299
+ "tags": tags,
300
  "x_post": social.get("x_post", ""),
301
  "linkedin_post": social.get("linkedin_post", ""),
302
  }
303
  await _db_insert_journal(entry)
304
+ return entry
305
 
306
+ # ── GRADIO ADMIN CONSOLE ──────────────────────────────────────────────────────
307
 
308
+ def _dummy_run():
309
+ return "ForgeSight Admin active."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
 
311
+ with gr.Blocks(title="ForgeSight Admin") as demo:
312
+ gr.Markdown("# 🔍 ForgeSight Control Center")
313
+ gr.Markdown("FastAPI backend is serving the REST API. Gradio is for admin tasks.")
314
+ btn = gr.Button("Ping")
315
+ out = gr.Textbox()
316
+ btn.click(fn=_dummy_run, outputs=out)
317
 
318
+ # Mount Gradio
319
+ app = gr.mount_gradio_app(app, demo, path="/gradio")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
  if __name__ == "__main__":
322
+ import uvicorn
323
+ uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
backend/deploy_to_amd.sh CHANGED
@@ -60,7 +60,7 @@ MONGO_URL=mongodb://localhost:27017
60
  DB_NAME=forgesight
61
  CORS_ORIGINS=*
62
  # Set your AMD vLLM inference server URL here:
63
- AMD_INFERENCE_URL=http://165.245.137.80
64
  AMD_INFERENCE_TOKEN=DiPipPSZoxb96rcrP7X+B0N5mTTEzxU/ziesgI/Z2NPo9xPKM
65
  AMD_MODEL_NAME=Qwen/Qwen2-VL-7B-Instruct
66
  EOF
 
60
  DB_NAME=forgesight
61
  CORS_ORIGINS=*
62
  # Set your AMD vLLM inference server URL here:
63
+ AMD_INFERENCE_URL=http://129.212.189.214
64
  AMD_INFERENCE_TOKEN=DiPipPSZoxb96rcrP7X+B0N5mTTEzxU/ziesgI/Z2NPo9xPKM
65
  AMD_MODEL_NAME=Qwen/Qwen2-VL-7B-Instruct
66
  EOF
backend/requirements.txt CHANGED
@@ -26,3 +26,6 @@ jq>=1.6.0
26
  typer>=0.9.0
27
  httpx>=0.27.0
28
  aiohttp>=3.9.0
 
 
 
 
26
  typer>=0.9.0
27
  httpx>=0.27.0
28
  aiohttp>=3.9.0
29
+ gradio==4.26.0
30
+ fpdf==1.7.2
31
+ certifi==2024.2.2
frontend/src/components/AgentTranscript.jsx CHANGED
@@ -1,11 +1,12 @@
1
  import { useEffect, useState } from "react";
2
- import { Eye, Stethoscope, Wrench, FileText, CheckCircle2, AlertTriangle, XCircle, WifiOff } from "lucide-react";
3
 
4
  const ICONS = {
5
  inspector: Eye,
6
  diagnostician: Stethoscope,
7
  action: Wrench,
8
  reporter: FileText,
 
9
  };
10
 
11
  const VERDICT_CONFIG = {
@@ -178,6 +179,50 @@ function ReporterOutput({ parsed }) {
178
  );
179
  }
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  function AgentContent({ agent, isMock }) {
182
  const { role, output } = agent;
183
  const parsed = output?.parsed || {};
@@ -185,6 +230,7 @@ function AgentContent({ agent, isMock }) {
185
  if (role === "diagnostician") return <DiagnosticianOutput parsed={parsed} />;
186
  if (role === "action") return <ActionOutput parsed={parsed} />;
187
  if (role === "reporter") return <ReporterOutput parsed={parsed} />;
 
188
  return <pre className="font-mono text-xs text-zinc-400 whitespace-pre-wrap break-words">{JSON.stringify(parsed, null, 2)}</pre>;
189
  }
190
 
 
1
  import { useEffect, useState } from "react";
2
+ import { Eye, Stethoscope, Wrench, FileText, Share2, CheckCircle2, AlertTriangle, XCircle, WifiOff, Twitter, Linkedin } from "lucide-react";
3
 
4
  const ICONS = {
5
  inspector: Eye,
6
  diagnostician: Stethoscope,
7
  action: Wrench,
8
  reporter: FileText,
9
+ social: Share2,
10
  };
11
 
12
  const VERDICT_CONFIG = {
 
179
  );
180
  }
181
 
182
+ function SocialOutput({ parsed }) {
183
+ const xText = parsed?.x_post || "";
184
+ const linkedInText = parsed?.linkedin_post || "";
185
+ return (
186
+ <div className="grid md:grid-cols-2 gap-4">
187
+ <div className="p-4 border border-white/5 bg-[#141416] rounded-sm fs-rise">
188
+ <div className="flex items-center gap-2 mb-3">
189
+ <Twitter className="w-4 h-4 text-[#1DA1F2]" />
190
+ <span className="font-mono text-[10px] text-zinc-500 uppercase tracking-widest">X / Twitter</span>
191
+ </div>
192
+ <p className="text-xs text-zinc-300 font-mono leading-relaxed">{xText}</p>
193
+ <div className="mt-4 flex justify-end">
194
+ <button
195
+ onClick={() => window.open(`https://twitter.com/intent/tweet?text=${encodeURIComponent(xText)}`, '_blank')}
196
+ className="font-mono text-[10px] px-2 py-1 border border-white/10 hover:bg-white/5 transition-colors text-zinc-400"
197
+ >
198
+ Draft Post
199
+ </button>
200
+ </div>
201
+ </div>
202
+ <div className="p-4 border border-white/5 bg-[#141416] rounded-sm fs-rise">
203
+ <div className="flex items-center gap-2 mb-3">
204
+ <Linkedin className="w-4 h-4 text-[#0A66C2]" />
205
+ <span className="font-mono text-[10px] text-zinc-500 uppercase tracking-widest">LinkedIn</span>
206
+ </div>
207
+ <div className="text-[11px] text-zinc-400 font-sans whitespace-pre-wrap leading-relaxed max-h-32 overflow-y-auto pr-2 custom-scrollbar">
208
+ {linkedInText}
209
+ </div>
210
+ <div className="mt-4 flex justify-end">
211
+ <button
212
+ onClick={() => {
213
+ navigator.clipboard.writeText(linkedInText);
214
+ alert("LinkedIn text copied!");
215
+ }}
216
+ className="font-mono text-[10px] px-2 py-1 border border-white/10 hover:bg-white/5 transition-colors text-zinc-400"
217
+ >
218
+ Copy Text
219
+ </button>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ );
224
+ }
225
+
226
  function AgentContent({ agent, isMock }) {
227
  const { role, output } = agent;
228
  const parsed = output?.parsed || {};
 
230
  if (role === "diagnostician") return <DiagnosticianOutput parsed={parsed} />;
231
  if (role === "action") return <ActionOutput parsed={parsed} />;
232
  if (role === "reporter") return <ReporterOutput parsed={parsed} />;
233
+ if (role === "social") return <SocialOutput parsed={parsed} />;
234
  return <pre className="font-mono text-xs text-zinc-400 whitespace-pre-wrap break-words">{JSON.stringify(parsed, null, 2)}</pre>;
235
  }
236
 
frontend/src/index.css CHANGED
@@ -49,16 +49,41 @@ code {
49
  }
50
  }
51
 
52
- @layer base {
53
- [data-debug-wrapper="true"] {
54
- display: contents !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
- [data-debug-wrapper="true"] > * {
57
- margin-left: inherit; margin-right: inherit; margin-top: inherit; margin-bottom: inherit;
58
- padding-left: inherit; padding-right: inherit; padding-top: inherit; padding-bottom: inherit;
59
- column-gap: inherit; row-gap: inherit; gap: inherit;
60
- border-left-width: inherit; border-right-width: inherit; border-top-width: inherit; border-bottom-width: inherit;
61
- border-left-style: inherit; border-right-style: inherit; border-top-style: inherit; border-bottom-style: inherit;
62
- border-left-color: inherit; border-right-color: inherit; border-top-color: inherit; border-bottom-color: inherit;
 
 
 
 
 
 
 
 
 
63
  }
64
  }
 
 
49
  }
50
  }
51
 
52
+ @layer utilities {
53
+ .glass {
54
+ @apply bg-white/[0.03] border border-white/10 backdrop-blur-md;
55
+ }
56
+ .fs-glow {
57
+ box-shadow: 0 0 20px -5px rgba(237, 28, 36, 0.2);
58
+ }
59
+ .fs-glow-intense {
60
+ box-shadow: 0 0 40px -10px rgba(237, 28, 36, 0.4);
61
+ }
62
+ .fs-label {
63
+ @apply font-mono text-[10px] uppercase tracking-widest text-zinc-500;
64
+ }
65
+ .fs-chip {
66
+ @apply font-mono text-[10px] px-2 py-0.5 border rounded-sm;
67
+ }
68
+ .fs-chip-pass {
69
+ @apply border-[#10B981]/30 text-[#10B981] bg-[#10B981]/5;
70
  }
71
+ .fs-chip-fail {
72
+ @apply border-[#ED1C24]/30 text-[#ED1C24] bg-[#ED1C24]/5;
73
+ }
74
+ .custom-scrollbar::-webkit-scrollbar {
75
+ width: 4px;
76
+ height: 4px;
77
+ }
78
+ .custom-scrollbar::-webkit-scrollbar-track {
79
+ background: transparent;
80
+ }
81
+ .custom-scrollbar::-webkit-scrollbar-thumb {
82
+ background: #333;
83
+ border-radius: 10px;
84
+ }
85
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
86
+ background: #ED1C24;
87
  }
88
  }
89
+
frontend/src/pages/Blueprint.jsx CHANGED
@@ -1,18 +1,15 @@
1
  import { useEffect, useState, useRef } from "react";
2
  import mermaid from "mermaid";
3
  import { forgesight } from "@/lib/api";
4
- import { Cpu, HardDrive, Server, BookOpen, Bot, Rocket, ArrowDown } from "lucide-react";
5
 
6
  const LAYER_ICONS = {
7
  Hardware: Cpu, Runtime: HardDrive, Serving: Server,
8
  Model: BookOpen, Agents: Bot, Product: Rocket,
9
  };
10
 
11
- const BLUEPRINT_IMG = "https://static.prod-images.emergentagent.com/jobs/d5829a2e-bc03-4880-adcd-73acc809a3bd/images/7251062dc0e36ea4218374b05cc959bc4e6c55a2cf4789a8a2cbc38db6392916.png";
12
-
13
  export default function Blueprint() {
14
  const [data, setData] = useState(null);
15
-
16
  const mermaidRef = useRef(null);
17
 
18
  useEffect(() => {
@@ -20,13 +17,17 @@ export default function Blueprint() {
20
 
21
  mermaid.initialize({
22
  theme: "dark",
 
 
23
  themeVariables: {
24
  primaryColor: "#ED1C24",
25
  primaryTextColor: "#fff",
26
  primaryBorderColor: "#ED1C24",
27
- lineColor: "#ED1C24",
28
  secondaryColor: "#141416",
29
  tertiaryColor: "#0A0A0A",
 
 
30
  },
31
  });
32
  }, []);
@@ -40,127 +41,176 @@ export default function Blueprint() {
40
  const pipelineDiagram = `
41
  graph TD
42
  subgraph "Data Acquisition"
43
- IMG[Image / Video] -->|Base64| API[FastAPI / Gradio]
44
  end
45
 
46
- subgraph "MI300X + ROCm Inference"
47
- API -->|vLLM Request| VLLM[vLLM / Qwen2-VL]
48
- VLLM -->|JSON Response| API
 
49
  end
50
 
51
  subgraph "Agentic Pipeline"
52
- I[Inspector] -->|Defects JSON| D[Diagnostician]
53
- D -->|Root Cause JSON| A[Action]
54
- A -->|Work Order JSON| R[Reporter]
55
- R -->|Final Summary| UI[React UI]
 
56
  end
57
 
58
- API -.-> I
59
- classDef default font-family:Inter,color:#fff,fill:#0d0d10,stroke:#333
60
- classDef accent fill:#ED1C24,stroke:#ED1C24,color:#fff
61
- class VLLM,I,D,A,R accent
 
 
 
 
 
 
 
 
 
62
  `;
63
 
64
  return (
65
- <div className="mx-auto max-w-[1400px] px-6 py-10" data-testid="blueprint-page">
66
- <header className="mb-10 grid md:grid-cols-2 gap-10">
67
- <div>
68
- <div className="fs-label mb-3">§ BLUEPRINT · DEPLOYMENT STACK</div>
69
- <h1 className="font-display font-black tracking-tighter text-4xl md:text-5xl">
70
- The exact stack<br />we ship on MI300X.
71
- </h1>
72
- <p className="text-zinc-400 mt-4 max-w-lg">
73
- Six layers. Zero CUDA lock-in. Every choice is justified against the constraints
74
- of a factory-floor deployment: latency, privacy, and model memory footprint.
75
- </p>
76
- </div>
77
- <div className="border border-white/10 bg-[#0A0A0A] p-6 fs-corners flex items-center justify-center min-h-[300px]">
78
- <div className="mermaid w-full text-center" ref={mermaidRef}>
79
- {pipelineDiagram}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  </div>
81
  </div>
82
  </header>
83
 
84
- <section className="mb-16">
85
- <div className="fs-label mb-6">Stack · top to bottom</div>
86
- <div className="border-l-2 border-[#ED1C24] pl-0">
 
 
 
 
 
 
 
 
87
  {data?.stack?.map((layer, i) => {
88
  const Icon = LAYER_ICONS[layer.layer] || Cpu;
89
  return (
90
- <div key={i} className="relative">
91
- <div className="grid md:grid-cols-12 gap-6 border-b border-white/10 p-6 hover:bg-[#141416] transition-colors">
92
- <div className="md:col-span-2 flex items-start gap-3">
93
- <div className="w-9 h-9 border border-[#ED1C24] text-[#ED1C24] flex items-center justify-center">
94
- <Icon className="w-4 h-4" />
95
- </div>
96
- <div>
97
- <div className="fs-mono-small text-zinc-500">LAYER {String(i + 1).padStart(2, "0")}</div>
98
- <div className="font-display font-bold text-sm">{layer.layer}</div>
99
- </div>
100
- </div>
101
- <div className="md:col-span-4">
102
- <div className="font-display font-black tracking-tight text-xl">{layer.title}</div>
103
- <div className="font-mono text-xs text-zinc-500 mt-1">{layer.detail}</div>
104
  </div>
105
- <div className="md:col-span-6 text-sm text-zinc-400 leading-relaxed">{layer.why}</div>
 
 
 
 
 
 
 
 
 
106
  </div>
107
- {i < (data?.stack?.length || 0) - 1 && (
108
- <div className="flex justify-start pl-4 -mt-2 -mb-2">
109
- <ArrowDown className="w-3.5 h-3.5 text-[#ED1C24]" />
110
- </div>
111
- )}
112
  </div>
113
  );
114
  })}
115
  </div>
116
  </section>
117
 
 
118
  {data?.finetune_recipe && (
119
- <section className="border border-white/10 bg-[#141416] p-8 fs-corners" data-testid="finetune-recipe">
120
- <div className="flex items-end justify-between mb-6 flex-wrap gap-3">
121
- <div>
122
- <div className="fs-label mb-2">§ FINE-TUNE RECIPE · TRACK 2</div>
123
- <h2 className="font-display font-black tracking-tighter text-2xl md:text-3xl">QLoRA on Qwen2-VL</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  </div>
125
- <span className="fs-chip fs-chip-fail">MI300X · 8× GPU</span>
126
- </div>
127
- <div className="grid md:grid-cols-2 gap-0 border-t border-l border-white/10">
128
- <Cell k="BASE MODEL" v={data.finetune_recipe.base_model} />
129
- <Cell k="DATASET" v={data.finetune_recipe.dataset} />
130
- <Cell k="METHOD" v={data.finetune_recipe.method} />
131
- <Cell k="HARDWARE" v={data.finetune_recipe.hardware} />
132
- <Cell k="WALL CLOCK" v={data.finetune_recipe.expected_wall_clock} />
133
- <Cell k="SERVING" v={data.finetune_recipe.serve_with} />
134
  </div>
135
- <pre className="mt-8 font-mono text-[12px] leading-relaxed text-zinc-300 bg-[#0A0A0A] border border-white/10 p-5 overflow-x-auto">{`# ForgeSight fine-tune — MI300X + ROCm
136
- docker run --device=/dev/kfd --device=/dev/dri \\
137
- --security-opt seccomp=unconfined --group-add video \\
138
- rocm/pytorch:latest
139
-
140
- pip install "transformers>=4.45" "peft" "bitsandbytes" \\
141
- "optimum-amd" "datasets" "accelerate" "vllm"
142
-
143
- # train
144
- accelerate launch --mixed_precision bf16 train_qlora.py \\
145
- --base Qwen/Qwen2-VL-72B-Instruct \\
146
- --data forgesight/qc-10k \\
147
- --lora_r 64 --lora_alpha 128 \\
148
- --epochs 3 --batch_size 4 --grad_accum 8
149
-
150
- # serve
151
- vllm serve forgesight/qwen2-vl-72b-qc \\
152
- --tensor-parallel-size 8 --dtype bfloat16 --port 8000`}</pre>
153
  </section>
154
  )}
155
  </div>
156
  );
157
  }
158
 
159
- function Cell({ k, v }) {
 
 
 
 
 
 
 
 
 
160
  return (
161
- <div className="border-r border-b border-white/10 p-5">
162
- <div className="fs-label mb-2">{k}</div>
163
- <div className="font-mono text-sm text-white break-words">{v}</div>
 
 
 
164
  </div>
165
  );
166
  }
 
1
  import { useEffect, useState, useRef } from "react";
2
  import mermaid from "mermaid";
3
  import { forgesight } from "@/lib/api";
4
+ import { Cpu, HardDrive, Server, BookOpen, Bot, Rocket, ArrowRight, Terminal, Zap, ShieldCheck } from "lucide-react";
5
 
6
  const LAYER_ICONS = {
7
  Hardware: Cpu, Runtime: HardDrive, Serving: Server,
8
  Model: BookOpen, Agents: Bot, Product: Rocket,
9
  };
10
 
 
 
11
  export default function Blueprint() {
12
  const [data, setData] = useState(null);
 
13
  const mermaidRef = useRef(null);
14
 
15
  useEffect(() => {
 
17
 
18
  mermaid.initialize({
19
  theme: "dark",
20
+ startOnLoad: true,
21
+ securityLevel: "loose",
22
  themeVariables: {
23
  primaryColor: "#ED1C24",
24
  primaryTextColor: "#fff",
25
  primaryBorderColor: "#ED1C24",
26
+ lineColor: "#333",
27
  secondaryColor: "#141416",
28
  tertiaryColor: "#0A0A0A",
29
+ fontSize: "12px",
30
+ fontFamily: "JetBrains Mono",
31
  },
32
  });
33
  }, []);
 
41
  const pipelineDiagram = `
42
  graph TD
43
  subgraph "Data Acquisition"
44
+ IMG[Image Feed]
45
  end
46
 
47
+ subgraph "AMD MI300X Cluster"
48
+ VLLM[vLLM Engine]
49
+ QWEN[Qwen2-VL-7B]
50
+ VLLM --- QWEN
51
  end
52
 
53
  subgraph "Agentic Pipeline"
54
+ I[Inspector Agent]
55
+ D[Diagnose Agent]
56
+ A[Action Agent]
57
+ R[Report Agent]
58
+ I --> D --> A --> R
59
  end
60
 
61
+ IMG --> I
62
+ I -.-> VLLM
63
+ D -.-> VLLM
64
+ A -.-> VLLM
65
+ R -.-> VLLM
66
+
67
+ classDef device font-family:Inter,fill:#0d0d10,stroke:#333,color:#888
68
+ classDef compute fill:#ED1C24,stroke:#ED1C24,color:#fff,stroke-width:2px
69
+ classDef agent fill:#141416,stroke:#ED1C24,color:#fff,padding:10px
70
+
71
+ class IMG device
72
+ class VLLM,QWEN compute
73
+ class I,D,A,R agent
74
  `;
75
 
76
  return (
77
+ <div className="mx-auto max-w-[1400px] px-6 py-10 space-y-20" data-testid="blueprint-page">
78
+ {/* HERO SECTION */}
79
+ <header className="relative py-10 overflow-hidden">
80
+ <div className="absolute top-0 right-0 w-[600px] h-[600px] bg-[#ED1C24]/5 blur-[120px] rounded-full -translate-y-1/2 translate-x-1/4 -z-10" />
81
+
82
+ <div className="grid lg:grid-cols-2 gap-16 items-center">
83
+ <div className="space-y-6">
84
+ <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-[#ED1C24]/30 bg-[#ED1C24]/5 text-[#ED1C24] font-mono text-[10px] tracking-widest uppercase">
85
+ <Zap className="w-3 h-3" /> System Architecture
86
+ </div>
87
+ <h1 className="font-display font-black tracking-tighter text-5xl md:text-7xl leading-[0.9]">
88
+ Built for <span className="text-[#ED1C24]">Pure Performance.</span>
89
+ </h1>
90
+ <p className="text-zinc-400 text-lg max-w-lg leading-relaxed">
91
+ ForgeSight is architected to leverage the massive memory bandwidth of the AMD MI300X.
92
+ A six-layer stack designed for zero-latency industrial inference.
93
+ </p>
94
+ <div className="flex items-center gap-8 pt-4">
95
+ <Stat label="Hardware" value="MI300X" />
96
+ <Stat label="VRAM" value="192GB" />
97
+ <Stat label="Bandwidth" value="5.3 TB/s" />
98
+ </div>
99
+ </div>
100
+
101
+ <div className="glass p-8 fs-glow border-white/5 relative group">
102
+ <div className="absolute inset-0 bg-gradient-to-br from-[#ED1C24]/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
103
+ <div className="mermaid w-full overflow-hidden" ref={mermaidRef}>
104
+ {pipelineDiagram}
105
+ </div>
106
  </div>
107
  </div>
108
  </header>
109
 
110
+ {/* STACK LAYERS */}
111
+ <section>
112
+ <div className="flex items-end justify-between mb-10">
113
+ <div>
114
+ <div className="fs-label mb-2">The Stack</div>
115
+ <h2 className="font-display font-black text-3xl tracking-tight">Top-to-Bottom Integration</h2>
116
+ </div>
117
+ <div className="text-zinc-500 font-mono text-xs hidden md:block">06 TOTAL LAYERS</div>
118
+ </div>
119
+
120
+ <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
121
  {data?.stack?.map((layer, i) => {
122
  const Icon = LAYER_ICONS[layer.layer] || Cpu;
123
  return (
124
+ <div key={i} className="glass p-6 group hover:border-[#ED1C24]/50 transition-all duration-500 fs-glow">
125
+ <div className="flex items-start justify-between mb-6">
126
+ <div className="w-10 h-10 border border-[#ED1C24]/30 group-hover:border-[#ED1C24] text-[#ED1C24] flex items-center justify-center transition-colors">
127
+ <Icon className="w-5 h-5" />
 
 
 
 
 
 
 
 
 
 
128
  </div>
129
+ <span className="font-mono text-[10px] text-zinc-600">L{String(i + 1).padStart(2, "0")}</span>
130
+ </div>
131
+ <div className="space-y-2">
132
+ <div className="fs-label text-zinc-500">{layer.layer}</div>
133
+ <h3 className="font-display font-black text-xl group-hover:text-[#ED1C24] transition-colors">{layer.title}</h3>
134
+ <p className="text-sm text-zinc-400 leading-relaxed min-h-[60px]">{layer.why}</p>
135
+ </div>
136
+ <div className="mt-6 pt-6 border-t border-white/5">
137
+ <div className="font-mono text-[10px] text-zinc-500 mb-2 uppercase">Tech Spec</div>
138
+ <div className="text-xs text-white font-mono bg-white/5 px-2 py-1 inline-block">{layer.detail}</div>
139
  </div>
 
 
 
 
 
140
  </div>
141
  );
142
  })}
143
  </div>
144
  </section>
145
 
146
+ {/* FINETUNE RECIPE */}
147
  {data?.finetune_recipe && (
148
+ <section className="relative">
149
+ <div className="absolute inset-0 bg-[#ED1C24]/5 blur-[100px] -z-10" />
150
+ <div className="glass p-10 border-white/5 space-y-10">
151
+ <div className="flex items-start justify-between flex-wrap gap-6">
152
+ <div>
153
+ <div className="fs-label mb-2 flex items-center gap-2">
154
+ <Terminal className="w-3 h-3" /> Training Protocol
155
+ </div>
156
+ <h2 className="font-display font-black tracking-tighter text-4xl">QLoRA Optimization</h2>
157
+ <p className="text-zinc-400 mt-2">Maximum efficiency training recipe for Qwen2-VL-7B.</p>
158
+ </div>
159
+ <div className="flex items-center gap-3">
160
+ <div className="px-4 py-2 bg-[#ED1C24] text-white font-display font-black text-sm tracking-tight">8× MI300X</div>
161
+ <div className="px-4 py-2 border border-white/10 text-white font-mono text-xs">BF16 MIXED</div>
162
+ </div>
163
+ </div>
164
+
165
+ <div className="grid md:grid-cols-3 gap-8">
166
+ <SpecItem icon={BookOpen} label="Base Model" value={data.finetune_recipe.base_model} />
167
+ <SpecItem icon={Server} label="Serving Engine" value={data.finetune_recipe.serve_with} />
168
+ <SpecItem icon={ShieldCheck} label="Compute Platform" value={data.finetune_recipe.hardware} />
169
+ </div>
170
+
171
+ <div className="relative">
172
+ <div className="absolute top-4 right-4 flex gap-2">
173
+ <div className="w-2 h-2 rounded-full bg-zinc-700" />
174
+ <div className="w-2 h-2 rounded-full bg-zinc-700" />
175
+ <div className="w-2 h-2 rounded-full bg-zinc-700" />
176
+ </div>
177
+ <pre className="font-mono text-[13px] leading-relaxed text-zinc-300 bg-[#050505] border border-white/10 p-8 pt-12 overflow-x-auto custom-scrollbar shadow-2xl">
178
+ <code className="text-blue-400"># ForgeSight ROCm Optimized Fine-tune</code>{"\n"}
179
+ <code className="text-[#ED1C24]">accelerate launch</code> --mixed_precision bf16 train_qlora.py \{"\n"}
180
+ {" "}--base <span className="text-green-400">Qwen/Qwen2-VL-7B-Instruct</span> \{"\n"}
181
+ {" "}--data <span className="text-green-400">forgesight/qc-industrial-v1</span> \{"\n"}
182
+ {" "}--lora_r 64 --lora_alpha 128 \{"\n"}
183
+ {" "}--epochs 3 --batch_size 4 --grad_accum 8{"\n\n"}
184
+ <code className="text-blue-400"># Production Inference</code>{"\n"}
185
+ <code className="text-[#ED1C24]">vllm serve</code> forgesight/qwen2-vl-mi300x \{"\n"}
186
+ {" "}--enforce-eager --no-enable-chunked-prefill \{"\n"}
187
+ {" "}--dtype bfloat16 --port 8000
188
+ </pre>
189
  </div>
 
 
 
 
 
 
 
 
 
190
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  </section>
192
  )}
193
  </div>
194
  );
195
  }
196
 
197
+ function Stat({ label, value }) {
198
+ return (
199
+ <div className="space-y-1">
200
+ <div className="fs-label">{label}</div>
201
+ <div className="font-display font-black text-2xl text-white tracking-tighter">{value}</div>
202
+ </div>
203
+ );
204
+ }
205
+
206
+ function SpecItem({ icon: Icon, label, value }) {
207
  return (
208
+ <div className="flex gap-4 items-center p-4 bg-white/[0.02] border border-white/5">
209
+ <Icon className="w-5 h-5 text-[#ED1C24]" />
210
+ <div>
211
+ <div className="fs-label mb-0.5 text-zinc-500">{label}</div>
212
+ <div className="font-mono text-xs text-white">{value}</div>
213
+ </div>
214
  </div>
215
  );
216
  }
vercel.json CHANGED
@@ -6,7 +6,7 @@
6
  "framework": "create-react-app"
7
  },
8
  "backend": {
9
- "entrypoint": "backend",
10
  "routePrefix": "/_/backend",
11
  "framework": "python"
12
  }
 
6
  "framework": "create-react-app"
7
  },
8
  "backend": {
9
+ "entrypoint": "backend/app.py",
10
  "routePrefix": "/_/backend",
11
  "framework": "python"
12
  }