🐛 Fix: Resolve backend 500 on Vercel by implementing FastAPI and syncing dependencies
Browse files- backend/agents.py +16 -6
- backend/app.py +202 -366
- backend/deploy_to_amd.sh +1 -1
- backend/requirements.txt +3 -0
- frontend/src/components/AgentTranscript.jsx +47 -1
- frontend/src/index.css +35 -10
- frontend/src/pages/Blueprint.jsx +141 -91
- vercel.json +1 -1
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}/
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
from datetime import datetime, timezone
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
#
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 75 |
-
|
| 76 |
-
|
| 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 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
}
|
| 95 |
-
await _db_insert_inspection(inspection)
|
| 96 |
|
| 97 |
-
|
| 98 |
-
return json.dumps({
|
| 99 |
-
"id": inspection["id"],
|
| 100 |
-
"created_at": inspection["created_at"],
|
| 101 |
-
"transcript": transcript,
|
| 102 |
-
"summary": summary,
|
| 103 |
-
})
|
| 104 |
|
|
|
|
| 105 |
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
docs = await _db_list_inspections(limit)
|
| 109 |
items = [_summarize(doc) for doc in docs]
|
| 110 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
|
| 114 |
-
async def
|
| 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
|
| 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 |
-
|
| 151 |
-
async def
|
| 152 |
-
|
| 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 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 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 |
-
|
| 224 |
-
async def
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
if not docs:
|
| 228 |
await _seed_journal()
|
| 229 |
-
|
| 230 |
-
return
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
async def
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
try:
|
| 236 |
social = await generate_social_post(title, body)
|
| 237 |
-
except
|
| 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":
|
| 246 |
"x_post": social.get("x_post", ""),
|
| 247 |
"linkedin_post": social.get("linkedin_post", ""),
|
| 248 |
}
|
| 249 |
await _db_insert_journal(entry)
|
| 250 |
-
return
|
| 251 |
|
|
|
|
| 252 |
|
| 253 |
-
|
| 254 |
-
|
| 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 |
-
#
|
| 290 |
-
|
| 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
|
| 483 |
-
|
| 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://
|
| 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
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
}
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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: "#
|
| 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
|
| 44 |
end
|
| 45 |
|
| 46 |
-
subgraph "MI300X
|
| 47 |
-
|
| 48 |
-
|
|
|
|
| 49 |
end
|
| 50 |
|
| 51 |
subgraph "Agentic Pipeline"
|
| 52 |
-
I[Inspector
|
| 53 |
-
D
|
| 54 |
-
A
|
| 55 |
-
R
|
|
|
|
| 56 |
end
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
`;
|
| 63 |
|
| 64 |
return (
|
| 65 |
-
<div className="mx-auto max-w-[1400px] px-6 py-10" data-testid="blueprint-page">
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
<
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
</div>
|
| 81 |
</div>
|
| 82 |
</header>
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
<div className="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
{data?.stack?.map((layer, i) => {
|
| 88 |
const Icon = LAYER_ICONS[layer.layer] || Cpu;
|
| 89 |
return (
|
| 90 |
-
<div key={i} className="
|
| 91 |
-
<div className="
|
| 92 |
-
<div className="
|
| 93 |
-
<
|
| 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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 120 |
-
<div className="
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
return (
|
| 161 |
-
<div className="
|
| 162 |
-
<
|
| 163 |
-
<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 |
}
|