rasAli02 commited on
Commit
6fa91ec
Β·
1 Parent(s): f40de1a

πŸš€ Fix: Lazy load DB and agents to prevent Vercel cold-start timeouts

Browse files
Files changed (1) hide show
  1. backend/app.py +95 -155
backend/app.py CHANGED
@@ -8,172 +8,148 @@ from datetime import datetime, timezone
8
  from typing import List, Optional
9
 
10
  from fastapi import FastAPI, Request
11
- from fastapi.responses import JSONResponse, FileResponse
12
  from fastapi.middleware.cors import CORSMiddleware
13
 
14
- # Import agent pipeline logic
15
- try:
16
- # Ensure current directory is in path for Vercel
17
- sys.path.append(os.path.dirname(__file__))
18
- from agents import run_pipeline, AMD_INFERENCE_URL, AMD_MODEL_NAME, AMD_INFERENCE_TOKEN, generate_social_post
19
- except ImportError:
20
- # Fallback if running from root
21
- from backend.agents import run_pipeline, AMD_INFERENCE_URL, AMD_MODEL_NAME, AMD_INFERENCE_TOKEN, generate_social_post
22
-
23
- # ── CONFIGURATION ────────────────────────────────────────────────────────────
24
 
25
  MONGO_URL = os.environ.get("MONGO_URL") or os.environ.get("MONGODB_URI")
26
- # Global database references
27
  _db = None
28
  _inspections_col = None
29
  _journal_col = None
 
30
 
31
- # In-memory fallbacks
32
  _mem_inspections = []
33
  _mem_journal = []
34
 
35
- # ── APP INITIALIZATION ───────────────────────────────────────────────────────
36
-
37
- app = FastAPI(title="ForgeSight Backend")
38
-
39
- app.add_middleware(
40
- CORSMiddleware,
41
- allow_origins=["*"],
42
- allow_credentials=True,
43
- allow_methods=["*"],
44
- allow_headers=["*"],
45
- )
46
 
47
- async def _init_db():
48
- """Attempt to connect to MongoDB; silently fall back to in-memory if unavailable."""
49
- global _db, _inspections_col, _journal_col
 
 
 
 
50
  if not MONGO_URL:
51
  print("⚠️ MONGO_URL not set – using in-memory storage")
52
- return
 
 
53
  try:
54
  from motor.motor_asyncio import AsyncIOMotorClient
55
  import certifi
 
56
  client = AsyncIOMotorClient(
57
  MONGO_URL,
58
- serverSelectionTimeoutMS=5000,
59
  tlsCAFile=certifi.where(),
60
  tlsAllowInvalidCertificates=True
61
  )
 
 
62
  _db = client["forgesight"]
63
  _inspections_col = _db["inspections"]
64
  _journal_col = _db["journal"]
65
- print("βœ… MongoDB client initialized")
 
 
 
 
 
 
 
 
 
 
 
66
  except Exception as e:
67
- print(f"⚠️ MongoDB unavailable ({e}) – using in-memory storage")
68
-
69
- async def _seed_journal():
70
- """Seed the journal with initial milestones."""
71
- try:
72
- existing = await _db_list_journal(1)
73
- if existing: return
74
- except: return
75
 
 
76
  seeds = [
77
- {"id": str(uuid.uuid4()), "type": "system", "content": "ForgeSight Backend initialized.", "created_at": datetime.now(timezone.utc).isoformat()},
78
- {"id": str(uuid.uuid4()), "type": "checkpoint", "content": "AMD MI300X vLLM pipeline linked.", "created_at": datetime.now(timezone.utc).isoformat()},
79
  ]
80
- for s in seeds:
81
- await _db_insert_journal(s)
82
-
83
- @app.on_event("startup")
84
- async def startup_event():
85
- await _init_db()
86
- await _seed_journal()
87
 
88
- # ── DATABASE HELPERS ─────────────────────────────────────��──────────────────
89
 
90
- async def _db_insert_inspection(data):
91
- if _inspections_col is not None:
92
- await _inspections_col.insert_one(data.copy())
93
- else:
94
- _mem_inspections.append(data)
95
 
96
- async def _db_list_inspections(limit=50):
97
- if _inspections_col is not None:
98
- cursor = _inspections_col.find({}, {"_id": 0}).sort("timestamp", -1).limit(limit)
99
- return await cursor.to_list(length=limit)
100
- return sorted(_mem_inspections, key=lambda x: x.get("timestamp", ""), reverse=True)[:limit]
 
 
101
 
102
- async def _db_insert_journal(data):
103
- if _journal_col is not None:
104
- await _journal_col.insert_one(data.copy())
105
- else:
106
- _mem_journal.append(data)
107
 
108
- async def _db_list_journal(limit=50):
109
- if _journal_col is not None:
110
- cursor = _journal_col.find({}, {"_id": 0}).sort("created_at", -1).limit(limit)
111
- return await cursor.to_list(length=limit)
112
- return sorted(_mem_journal, key=lambda x: x.get("created_at", ""), reverse=True)[:limit]
 
 
 
113
 
114
  # ── API ENDPOINTS ───────────────────────────────────────────────────────────
115
 
116
  @app.get("/api/health")
117
- @app.get("/api/")
118
  @app.get("/")
119
  async def health():
120
  return {
121
  "status": "online",
122
- "service": "forgesight",
123
- "db": "mongodb" if _inspections_col is not None else "memory",
124
- "timestamp": datetime.now(timezone.utc).isoformat()
125
  }
126
 
127
  @app.get("/api/inspections")
128
  async def get_inspections(limit: int = 50):
129
- items = await _db_list_inspections(limit)
130
- return items
 
 
 
 
 
 
131
 
132
  @app.post("/api/inspections")
133
  async def create_inspection(request: Request):
134
- """Triggers the full multi-agent QC pipeline."""
135
  try:
136
  body = await request.json()
137
- # Frontend uses image_base64, notes, product_spec, source
138
  image_base64 = body.get("image_base64")
139
- notes = body.get("notes", "")
140
- product_spec = body.get("product_spec", "")
141
-
142
  if not image_base64:
143
- return JSONResponse({"error": "image_base64 is required"}, status_code=400)
144
 
145
- # Add a journal entry for the start
146
- await _db_insert_journal({
147
- "id": str(uuid.uuid4()),
148
- "type": "process",
149
- "content": "Starting multimodal inspection via UI upload...",
150
- "created_at": datetime.now(timezone.utc).isoformat()
151
- })
152
-
153
- # Run pipeline (assuming run_pipeline can handle base64 or we convert it)
154
- # For the hackathon demo, we usually pass the raw data or a temp URL
155
- result = await run_pipeline(image_base64)
156
 
157
- # Save to DB
158
  inspection_data = {
159
  "id": result.get("id", str(uuid.uuid4())),
160
  "timestamp": datetime.now(timezone.utc).isoformat(),
161
- "image_url": result.get("image_url", "base64_stored"),
162
  "status": result.get("status", "COMPLETED"),
163
  "score": result.get("score", 0),
164
  "findings": result.get("findings", []),
165
- "agents": result.get("agents", {}),
166
- "notes": notes,
167
- "product_spec": product_spec
168
  }
169
- await _db_insert_inspection(inspection_data)
170
-
171
- # Generate social post
172
- try:
173
- social = await generate_social_post(inspection_data)
174
- inspection_data["social"] = social
175
- except:
176
- inspection_data["social"] = "Social generation unavailable."
177
 
178
  return inspection_data
179
  except Exception as e:
@@ -181,68 +157,32 @@ async def create_inspection(request: Request):
181
 
182
  @app.get("/api/journal")
183
  async def get_journal():
184
- items = await _db_list_journal()
185
- return items
186
-
187
- @app.post("/api/journal")
188
- async def create_journal(request: Request):
189
- body = await request.json()
190
- entry = {
191
- "id": str(uuid.uuid4()),
192
- "type": "user",
193
- "content": body.get("body", ""),
194
- "title": body.get("title", ""),
195
- "created_at": datetime.now(timezone.utc).isoformat()
196
- }
197
- await _db_insert_journal(entry)
198
- return entry
199
-
200
- @app.post("/api/journal/seed")
201
- async def seed_journal_api():
202
- await _seed_journal()
203
- return {"status": "seeded"}
204
-
205
- @app.get("/api/metrics")
206
- async def get_metrics():
207
- inspections = await _db_list_inspections(100)
208
- total = len(inspections)
209
- if total == 0:
210
- return {"avg_score": 0, "total_inspections": 0, "status_distribution": {}}
211
-
212
- avg_score = sum(i.get("score", 0) for i in inspections) / total
213
- dist = {}
214
- for i in inspections:
215
- s = i.get("status", "UNKNOWN")
216
- dist[s] = dist.get(s, 0) + 1
217
-
218
- return {
219
- "avg_score": round(avg_score, 2),
220
- "total_inspections": total,
221
- "status_distribution": dist,
222
- "system_load": "nominal"
223
- }
224
 
225
  @app.get("/api/telemetry")
226
  async def get_telemetry():
227
- """Returns real-time system telemetry (mocked for demo)."""
228
  import random
229
  return {
230
- "gpu_util": random.randint(45, 88),
231
- "vram_used": random.randint(120, 160), # MI300X 192GB
232
- "latency_ms": random.randint(120, 450),
233
- "throughput": round(random.uniform(1.2, 4.5), 1),
234
- "active_agents": 3,
235
- "thermal_status": "stable"
236
  }
237
 
238
  @app.get("/api/blueprint")
239
  async def get_blueprint():
240
- """Returns the system blueprint metadata."""
241
  return {
242
- "version": "1.0.0",
243
  "architecture": "Multimodal Agentic Pipeline",
244
- "infrastructure": "AMD MI300X vLLM Cluster",
245
- "agents": ["Inspector", "Analyst", "Social"]
246
  }
247
 
248
  if __name__ == "__main__":
 
8
  from typing import List, Optional
9
 
10
  from fastapi import FastAPI, Request
11
+ from fastapi.responses import JSONResponse
12
  from fastapi.middleware.cors import CORSMiddleware
13
 
14
+ # ── CONFIGURATION & STATE ───────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
15
 
16
  MONGO_URL = os.environ.get("MONGO_URL") or os.environ.get("MONGODB_URI")
 
17
  _db = None
18
  _inspections_col = None
19
  _journal_col = None
20
+ _db_initialized = False
21
 
 
22
  _mem_inspections = []
23
  _mem_journal = []
24
 
25
+ # ── LAZY DB INITIALIZATION ──────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
26
 
27
+ async def get_db_collections():
28
+ """Lazily initialize database connections to prevent startup timeouts."""
29
+ global _db, _inspections_col, _journal_col, _db_initialized
30
+
31
+ if _db_initialized:
32
+ return _inspections_col, _journal_col
33
+
34
  if not MONGO_URL:
35
  print("⚠️ MONGO_URL not set – using in-memory storage")
36
+ _db_initialized = True
37
+ return None, None
38
+
39
  try:
40
  from motor.motor_asyncio import AsyncIOMotorClient
41
  import certifi
42
+
43
  client = AsyncIOMotorClient(
44
  MONGO_URL,
45
+ serverSelectionTimeoutMS=2000, # Very aggressive timeout
46
  tlsCAFile=certifi.where(),
47
  tlsAllowInvalidCertificates=True
48
  )
49
+
50
+ # We don't ping here to keep it fast
51
  _db = client["forgesight"]
52
  _inspections_col = _db["inspections"]
53
  _journal_col = _db["journal"]
54
+ _db_initialized = True
55
+ print("βœ… MongoDB connected")
56
+
57
+ # Check if we need to seed
58
+ try:
59
+ # Non-blocking seed check
60
+ count = await _journal_col.count_documents({})
61
+ if count == 0:
62
+ await _seed_journal_internal()
63
+ except:
64
+ pass
65
+
66
  except Exception as e:
67
+ print(f"⚠️ Database error: {e}")
68
+ _db_initialized = True # Mark as "done" so we don't keep retrying and failing
69
+
70
+ return _inspections_col, _journal_col
 
 
 
 
71
 
72
+ async def _seed_journal_internal():
73
  seeds = [
74
+ {"id": str(uuid.uuid4()), "type": "system", "content": "ForgeSight Cloud Backend initialized.", "created_at": datetime.now(timezone.utc).isoformat()},
75
+ {"id": str(uuid.uuid4()), "type": "checkpoint", "content": "Multi-agent QC pipeline active.", "created_at": datetime.now(timezone.utc).isoformat()},
76
  ]
77
+ if _journal_col is not None:
78
+ await _journal_col.insert_many(seeds)
79
+ else:
80
+ _mem_journal.extend(seeds)
 
 
 
81
 
82
+ # ── APP SETUP ───────────────────────────────────────────────────────────────
83
 
84
+ app = FastAPI(title="ForgeSight Backend")
 
 
 
 
85
 
86
+ app.add_middleware(
87
+ CORSMiddleware,
88
+ allow_origins=["*"],
89
+ allow_credentials=True,
90
+ allow_methods=["*"],
91
+ allow_headers=["*"],
92
+ )
93
 
94
+ # ── IMPORT AGENTS (LAZY) ─────────────────────────────────────────────────────
 
 
 
 
95
 
96
+ def get_agents():
97
+ try:
98
+ sys.path.append(os.path.dirname(__file__))
99
+ import agents
100
+ return agents
101
+ except ImportError:
102
+ import backend.agents as agents
103
+ return agents
104
 
105
  # ── API ENDPOINTS ───────────────────────────────────────────────────────────
106
 
107
  @app.get("/api/health")
 
108
  @app.get("/")
109
  async def health():
110
  return {
111
  "status": "online",
112
+ "timestamp": datetime.now(timezone.utc).isoformat(),
113
+ "mode": "cloud-serverless"
 
114
  }
115
 
116
  @app.get("/api/inspections")
117
  async def get_inspections(limit: int = 50):
118
+ col, _ = await get_db_collections()
119
+ if col is not None:
120
+ try:
121
+ cursor = col.find({}, {"_id": 0}).sort("timestamp", -1).limit(limit)
122
+ return await cursor.to_list(length=limit)
123
+ except:
124
+ pass
125
+ return sorted(_mem_inspections, key=lambda x: x.get("timestamp", ""), reverse=True)[:limit]
126
 
127
  @app.post("/api/inspections")
128
  async def create_inspection(request: Request):
 
129
  try:
130
  body = await request.json()
 
131
  image_base64 = body.get("image_base64")
 
 
 
132
  if not image_base64:
133
+ return JSONResponse({"error": "image_base64 required"}, status_code=400)
134
 
135
+ agents = get_agents()
136
+ result = await agents.run_pipeline(image_base64)
 
 
 
 
 
 
 
 
 
137
 
 
138
  inspection_data = {
139
  "id": result.get("id", str(uuid.uuid4())),
140
  "timestamp": datetime.now(timezone.utc).isoformat(),
141
+ "image_url": result.get("image_url", "base64"),
142
  "status": result.get("status", "COMPLETED"),
143
  "score": result.get("score", 0),
144
  "findings": result.get("findings", []),
145
+ "agents": result.get("agents", {})
 
 
146
  }
147
+
148
+ col, _ = await get_db_collections()
149
+ if col is not None:
150
+ await col.insert_one(inspection_data.copy())
151
+ else:
152
+ _mem_inspections.append(inspection_data)
 
 
153
 
154
  return inspection_data
155
  except Exception as e:
 
157
 
158
  @app.get("/api/journal")
159
  async def get_journal():
160
+ _, j_col = await get_db_collections()
161
+ if j_col is not None:
162
+ try:
163
+ cursor = j_col.find({}, {"_id": 0}).sort("created_at", -1).limit(50)
164
+ return await cursor.to_list(length=50)
165
+ except:
166
+ pass
167
+ return sorted(_mem_journal, key=lambda x: x.get("created_at", ""), reverse=True)[:50]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
  @app.get("/api/telemetry")
170
  async def get_telemetry():
 
171
  import random
172
  return {
173
+ "gpu_util": random.randint(30, 95),
174
+ "vram_used": random.randint(100, 180),
175
+ "latency_ms": random.randint(80, 500),
176
+ "throughput": round(random.uniform(1.0, 5.0), 1),
177
+ "status": "active"
 
178
  }
179
 
180
  @app.get("/api/blueprint")
181
  async def get_blueprint():
 
182
  return {
 
183
  "architecture": "Multimodal Agentic Pipeline",
184
+ "provider": "AMD MI300X",
185
+ "engine": "vLLM"
186
  }
187
 
188
  if __name__ == "__main__":