rasAli02 commited on
Commit
e66e652
ยท
1 Parent(s): a88f575

๐Ÿš€ Fix: Resolve inspection failure (base64 data cleanup + agent alignment)

Browse files
Files changed (3) hide show
  1. agents.pyHeader +346 -0
  2. backend/agents.py +14 -0
  3. backend/app.py +20 -7
agents.pyHeader ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ForgeSight multi-agent quality-control pipeline.
3
+ Agents call the fine-tuned model served by vLLM on AMD Instinct MI300X.
4
+ Falls back to mock responses if the AMD inference server is unreachable.
5
+ """
6
+ import os
7
+ import json
8
+ import uuid
9
+ import re
10
+ import asyncio
11
+ from typing import Optional, List, Dict, Any
12
+
13
+ import httpx # async HTTP โ€” lightweight, no extra deps beyond requirements
14
+
15
+ # โ”€โ”€ AMD vLLM inference endpoint โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
16
+ # vLLM exposes an OpenAI-compatible API at /v1/chat/completions.
17
+ # Set AMD_INFERENCE_URL in your .env to point at the running vLLM server.
18
+ # Example: http://165.245.143.46:8000 (direct port โ€” ensure firewall allows it)
19
+ # Or use the Jupyter proxy route: http://165.245.143.46/proxy/8000
20
+ AMD_INFERENCE_URL = os.environ.get(
21
+ "AMD_INFERENCE_URL",
22
+ "http://129.212.189.214/proxy/8000"
23
+ ).rstrip("/")
24
+
25
+ # Token for the AMD inference server (if required)
26
+ AMD_INFERENCE_TOKEN = os.environ.get(
27
+ "AMD_INFERENCE_TOKEN",
28
+ "5peRa6unb0DdXvzB3Pbck48IgNTDmxeJSUvE4NdnhvW70FcaX"
29
+ )
30
+
31
+ # The model name vLLM is serving (used in the chat/completions request).
32
+ # Override with AMD_MODEL_NAME env var if you deploy a different checkpoint.
33
+ AMD_MODEL_NAME = os.environ.get("AMD_MODEL_NAME", "Qwen/Qwen2-VL-7B-Instruct")
34
+
35
+ # Timeout (seconds) to wait for the AMD server before falling back to mock.
36
+ AMD_TIMEOUT = float(os.environ.get("AMD_TIMEOUT", "60"))
37
+
38
+ # โ”€โ”€ System prompts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
39
+ INSPECTOR_SYSTEM = """You are the INSPECTOR agent of ForgeSight โ€” a multimodal quality-control copilot
40
+ running on AMD Instinct MI300X + ROCm. Your job: analyze the submitted construction site, road infrastructure, or housing
41
+ image and surface visible structural defects, safety hazards, anomalies, or code violations.
42
+
43
+ Return ONLY compact JSON with this exact shape (no prose, no code fences):
44
+ {
45
+ "verdict": "pass" | "warn" | "fail",
46
+ "confidence": 0.0-1.0,
47
+ "defects": [
48
+ {"type": "short category e.g. structural-crack", "severity": "low|medium|high", "location": "short spatial description", "description": "one sentence"}
49
+ ],
50
+ "observation": "2-3 sentence plain-english summary of what you see"
51
+ }
52
+ Be precise. If the image shows no construction/infrastructure issues at all, still describe what is visible
53
+ and mark verdict "warn" with a defect explaining the mismatch."""
54
+
55
+
56
+ DIAGNOSTICIAN_SYSTEM = """You are the DIAGNOSTICIAN agent of ForgeSight. Given the INSPECTOR's
57
+ JSON report and user notes, produce a probable root-cause analysis.
58
+
59
+ Return ONLY compact JSON:
60
+ {
61
+ "probable_cause": "one-sentence most likely cause",
62
+ "contributing_factors": ["factor 1", "factor 2", "factor 3"],
63
+ "affected_process_step": "e.g. concrete pouring, asphalt laying, framing"
64
+ }
65
+ Be concrete and industry-literate."""
66
+
67
+
68
+ ACTION_SYSTEM = """You are the ACTION agent of ForgeSight. Given the INSPECTOR and DIAGNOSTICIAN
69
+ outputs, draft an actionable work order.
70
+
71
+ Return ONLY compact JSON:
72
+ {
73
+ "priority": "P0|P1|P2|P3",
74
+ "assignee_role": "e.g. site-manager, structural-engineer, safety-officer",
75
+ "steps": ["step 1", "step 2", "step 3"],
76
+ "estimated_minutes": integer,
77
+ "parts_or_tools": ["item 1", "item 2"]
78
+ }"""
79
+
80
+
81
+ REPORTER_SYSTEM = """You are the REPORTER agent of ForgeSight. Compile a final human-readable
82
+ summary of the full inspection in <=70 words. Return ONLY JSON:
83
+ {
84
+ "headline": "<=10 word title",
85
+ "summary": "<=70 word paragraph",
86
+ "tags": ["tag1", "tag2", "tag3"]
87
+ }"""
88
+
89
+ SOCIAL_SYSTEM = """You craft punchy Build-in-Public social posts for a hackathon project named
90
+ "ForgeSight" โ€” a multimodal agentic quality-control copilot running on AMD Instinct MI300X + ROCm.
91
+ Always include hashtags: #AMDHackathon #ROCm #AIatAMD #lablab and mention @AIatAMD and @lablab.
92
+ Return ONLY JSON:
93
+ {"x_post": "<=260 chars, punchy, 1-2 emojis ok", "linkedin_post": "<=600 chars, narrative, 3 short paragraphs"}"""
94
+
95
+
96
+ # โ”€โ”€ JSON extraction โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
97
+ def _extract_json(raw: str) -> Dict[str, Any]:
98
+ """Best-effort JSON extraction from an LLM response."""
99
+ if not raw:
100
+ return {}
101
+ cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw.strip(), flags=re.MULTILINE)
102
+ try:
103
+ return json.loads(cleaned)
104
+ except Exception:
105
+ pass
106
+ match = re.search(r"\{[\s\S]*\}", cleaned)
107
+ if match:
108
+ try:
109
+ return json.loads(match.group(0))
110
+ except Exception:
111
+ pass
112
+ return {"_raw": raw}
113
+
114
+
115
+ # โ”€โ”€ Mock fallbacks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
116
+ def _mock_response(name: str) -> Dict[str, Any]:
117
+ """Fallback mock responses when AMD server is unreachable."""
118
+ mocks = {
119
+ "inspector": {
120
+ "verdict": "warn", "confidence": 0.85,
121
+ "defects": [{"type": "concrete-crack", "severity": "medium",
122
+ "location": "foundation wall, sector B", "description": "Diagonal hairline crack visible"}],
123
+ "observation": "Diagonal crack detected on the concrete foundation. [LOCAL MOCK โ€” AMD server offline]"
124
+ },
125
+ "diagnostician": {
126
+ "probable_cause": "Improper curing or settlement issues. [LOCAL MOCK]",
127
+ "contributing_factors": ["Temperature fluctuation", "Soil settlement"],
128
+ "affected_process_step": "Concrete curing"
129
+ },
130
+ "action": {
131
+ "priority": "P2", "assignee_role": "structural-engineer",
132
+ "steps": ["Assess crack depth", "Apply epoxy injection"],
133
+ "estimated_minutes": 120, "parts_or_tools": ["Epoxy resin", "Measurement gauge"]
134
+ },
135
+ "reporter": {
136
+ "headline": "Foundation Crack Detected [Mock]",
137
+ "summary": "Local mock response โ€” start the AMD vLLM server to use the fine-tuned model.",
138
+ "tags": ["crack", "concrete", "mock"]
139
+ },
140
+ "social": {
141
+ "x_post": "Testing our pipeline #AMDHackathon",
142
+ "linkedin_post": "We are testing our pipeline today..."
143
+ },
144
+ }
145
+ parsed = mocks.get(name, {})
146
+ return {"raw": json.dumps(parsed), "parsed": parsed, "source": "mock (AMD server offline)"}
147
+
148
+
149
+ # โ”€โ”€ AMD vLLM call (OpenAI-compatible /v1/chat/completions) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
150
+ async def _call_amd_vllm(
151
+ system_prompt: str,
152
+ user_text: str,
153
+ image_base64: Optional[str] = None,
154
+ ) -> Optional[str]:
155
+ """
156
+ Call the vLLM server on the AMD MI300X using its OpenAI-compatible API.
157
+ Supports vision models (image_base64) and text-only calls.
158
+ Returns the assistant message text, or None if the server is unreachable.
159
+ """
160
+ # Build messages array
161
+ # Clean base64 data: strip prefix if present
162
+ if image_base64 and "," in image_base64:
163
+ image_base64 = image_base64.split(",")[1]
164
+
165
+ if image_base64:
166
+ # Multimodal message with base64 image
167
+ user_content = [
168
+ {
169
+ "type": "image_url",
170
+ "image_url": {
171
+ "url": f"data:image/jpeg;base64,{image_base64}"
172
+ }
173
+ },
174
+ {
175
+ "type": "text",
176
+ "text": user_text
177
+ }
178
+ ]
179
+ else:
180
+ user_content = user_text
181
+
182
+ payload = {
183
+ "model": AMD_MODEL_NAME,
184
+ "messages": [
185
+ {"role": "system", "content": system_prompt},
186
+ {"role": "user", "content": user_content},
187
+ ],
188
+ "max_tokens": 1024,
189
+ "temperature": 0.1, # Low temperature for deterministic structured output
190
+ }
191
+
192
+ base_url = AMD_INFERENCE_URL.rstrip("/")
193
+ if not base_url.startswith("http"):
194
+ base_url = f"http://{base_url}"
195
+ if "/proxy/8000" not in base_url:
196
+ base_url = f"{base_url}/proxy/8000"
197
+ candidates = [
198
+ f"{base_url}/v1/chat/completions"
199
+ ]
200
+
201
+ headers = {}
202
+ if AMD_INFERENCE_TOKEN:
203
+ # Try both token and Bearer formats
204
+ headers["Authorization"] = f"token {AMD_INFERENCE_TOKEN}"
205
+
206
+ last_err = None
207
+ for url in candidates:
208
+ try:
209
+ async with httpx.AsyncClient(timeout=AMD_TIMEOUT) as client:
210
+ # Add token as param too just in case
211
+ test_url = f"{url}?token={AMD_INFERENCE_TOKEN}" if AMD_INFERENCE_TOKEN else url
212
+ resp = await client.post(test_url, json=payload, headers=headers)
213
+ if resp.status_code == 200:
214
+ data = resp.json()
215
+ return data["choices"][0]["message"]["content"]
216
+
217
+ # Try Bearer if token failed
218
+ headers["Authorization"] = f"Bearer {AMD_INFERENCE_TOKEN}"
219
+ resp = await client.post(test_url, json=payload, headers=headers)
220
+ if resp.status_code == 200:
221
+ data = resp.json()
222
+ return data["choices"][0]["message"]["content"]
223
+ except Exception as e:
224
+ last_err = e
225
+ continue
226
+
227
+ return None # All candidates failed
228
+
229
+
230
+ # โ”€โ”€ Agent runner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
231
+ async def _run_agent(
232
+ name: str,
233
+ system_message: str,
234
+ user_text: str,
235
+ image_base64: Optional[str] = None,
236
+ ) -> Dict[str, Any]:
237
+ """
238
+ Run a single agent. Tries AMD MI300X vLLM first, falls back to mock.
239
+ """
240
+ raw_text = await _call_amd_vllm(system_message, user_text, image_base64)
241
+
242
+ if raw_text is None:
243
+ # AMD server not reachable โ€” use local mock (safe for dev/demo)
244
+ result = _mock_response(name)
245
+ return result
246
+
247
+ # AMD server responded โ€” parse its JSON output
248
+ parsed = _extract_json(raw_text)
249
+ return {
250
+ "raw": raw_text,
251
+ "parsed": parsed,
252
+ "source": f"AMD MI300X vLLM @ {AMD_INFERENCE_URL} ({AMD_MODEL_NAME})"
253
+ }
254
+
255
+
256
+ # โ”€โ”€ Public pipeline โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
257
+ async def run_pipeline(
258
+ image_base64: str,
259
+ notes: str = "",
260
+ product_spec: str = "",
261
+ ) -> Dict[str, Any]:
262
+ """
263
+ Run the 4-agent pipeline sequentially and return the full transcript.
264
+ """
265
+ context = f"Operator notes: {notes or '(none)'}\nProduct spec: {product_spec or '(generic)'}"
266
+
267
+ # 1) Inspector (vision โ€” passes image to vLLM)
268
+ inspector = await _run_agent(
269
+ "inspector",
270
+ INSPECTOR_SYSTEM,
271
+ f"Inspect this image for manufacturing defects.\n{context}",
272
+ image_base64=image_base64,
273
+ )
274
+
275
+ # 2) Diagnostician (text only)
276
+ diagnostician = await _run_agent(
277
+ "diagnostician",
278
+ DIAGNOSTICIAN_SYSTEM,
279
+ f"INSPECTOR_REPORT:\n{json.dumps(inspector['parsed'])}\n\n{context}",
280
+ )
281
+
282
+ # 3) Action (text only)
283
+ action = await _run_agent(
284
+ "action",
285
+ ACTION_SYSTEM,
286
+ (
287
+ f"INSPECTOR_REPORT:\n{json.dumps(inspector['parsed'])}\n\n"
288
+ f"DIAGNOSTICIAN_REPORT:\n{json.dumps(diagnostician['parsed'])}"
289
+ ),
290
+ )
291
+
292
+ # 4) Reporter (text only)
293
+ reporter = await _run_agent(
294
+ "reporter",
295
+ REPORTER_SYSTEM,
296
+ (
297
+ f"INSPECTOR_REPORT:\n{json.dumps(inspector['parsed'])}\n\n"
298
+ f"DIAGNOSTICIAN_REPORT:\n{json.dumps(diagnostician['parsed'])}\n\n"
299
+ f"ACTION_REPORT:\n{json.dumps(action['parsed'])}"
300
+ ),
301
+ )
302
+
303
+ # 5) Social (text only)
304
+ social = await _run_agent(
305
+ "social",
306
+ SOCIAL_SYSTEM,
307
+ (
308
+ f"INSPECTOR_REPORT:\n{json.dumps(inspector['parsed'])}\n\n"
309
+ f"REPORTER_SUMMARY:\n{json.dumps(reporter['parsed'])}"
310
+ ),
311
+ )
312
+
313
+ model_label = AMD_MODEL_NAME
314
+ # Flatten important fields for the frontend
315
+ inspector_data = inspector.get("parsed", {})
316
+ reporter_data = reporter.get("parsed", {})
317
+
318
+ return {
319
+ "id": str(uuid.uuid4()),
320
+ "status": "COMPLETED",
321
+ "score": int(float(inspector_data.get("confidence", 0.8)) * 100),
322
+ "findings": inspector_data.get("defects", []),
323
+ "headline": reporter_data.get("headline", "Inspection Complete"),
324
+ "summary": reporter_data.get("summary", ""),
325
+ "agents": [
326
+ {"role": "inspector", "label": "Inspector Agent", "model": model_label, "output": inspector},
327
+ {"role": "diagnostician", "label": "Diagnostician Agent", "model": model_label, "output": diagnostician},
328
+ {"role": "action", "label": "Action Agent", "model": model_label, "output": action},
329
+ {"role": "reporter", "label": "Reporter Agent", "model": model_label, "output": reporter},
330
+ {"role": "social", "label": "Social Agent", "model": model_label, "output": social},
331
+ ],
332
+ }
333
+
334
+
335
+ async def generate_social_post(milestone_title: str, milestone_body: str) -> Dict[str, str]:
336
+ """Generate X + LinkedIn social post drafts for a build-in-public milestone."""
337
+ result = await _run_agent(
338
+ "social",
339
+ SOCIAL_SYSTEM,
340
+ f"Milestone: {milestone_title}\n\nDetails: {milestone_body}",
341
+ )
342
+ parsed = result["parsed"]
343
+ return {
344
+ "x_post": parsed.get("x_post", result["raw"][:260]),
345
+ "linkedin_post": parsed.get("linkedin_post", result["raw"][:600]),
346
+ }
backend/agents.py CHANGED
@@ -158,6 +158,10 @@ async def _call_amd_vllm(
158
  Returns the assistant message text, or None if the server is unreachable.
159
  """
160
  # Build messages array
 
 
 
 
161
  if image_base64:
162
  # Multimodal message with base64 image
163
  user_content = [
@@ -307,7 +311,17 @@ async def run_pipeline(
307
  )
308
 
309
  model_label = AMD_MODEL_NAME
 
 
 
 
310
  return {
 
 
 
 
 
 
311
  "agents": [
312
  {"role": "inspector", "label": "Inspector Agent", "model": model_label, "output": inspector},
313
  {"role": "diagnostician", "label": "Diagnostician Agent", "model": model_label, "output": diagnostician},
 
158
  Returns the assistant message text, or None if the server is unreachable.
159
  """
160
  # Build messages array
161
+ # Clean base64 data: strip prefix if present
162
+ if image_base64 and "," in image_base64:
163
+ image_base64 = image_base64.split(",")[1]
164
+
165
  if image_base64:
166
  # Multimodal message with base64 image
167
  user_content = [
 
311
  )
312
 
313
  model_label = AMD_MODEL_NAME
314
+ # Flatten important fields for the frontend
315
+ inspector_data = inspector.get("parsed", {})
316
+ reporter_data = reporter.get("parsed", {})
317
+
318
  return {
319
+ "id": str(uuid.uuid4()),
320
+ "status": "COMPLETED",
321
+ "score": int(float(inspector_data.get("confidence", 0.8)) * 100),
322
+ "findings": inspector_data.get("defects", []),
323
+ "headline": reporter_data.get("headline", "Inspection Complete"),
324
+ "summary": reporter_data.get("summary", ""),
325
  "agents": [
326
  {"role": "inspector", "label": "Inspector Agent", "model": model_label, "output": inspector},
327
  {"role": "diagnostician", "label": "Diagnostician Agent", "model": model_label, "output": diagnostician},
backend/app.py CHANGED
@@ -87,22 +87,35 @@ async def create_inspection(request: Request):
87
  try:
88
  body = await request.json()
89
  image_base64 = body.get("image_base64")
 
 
90
  if not image_base64:
91
  return JSONResponse({"error": "image_base64 required"}, status_code=400)
92
 
93
  agents = get_agents()
94
- result = await agents.run_pipeline(image_base64)
95
 
 
 
 
 
96
  inspection_data = {
97
- "id": result.get("id", str(uuid.uuid4())),
98
  "timestamp": datetime.now(timezone.utc).isoformat(),
99
- "image_url": result.get("image_url", "base64"),
100
- "status": result.get("status", "COMPLETED"),
101
- "score": result.get("score", 0),
102
- "findings": result.get("findings", []),
103
- "agents": result.get("agents", {})
104
  }
105
 
 
 
 
 
 
 
 
 
 
 
106
  col, _ = await get_db_collections()
107
  if col is not None:
108
  await col.insert_one(inspection_data.copy())
 
87
  try:
88
  body = await request.json()
89
  image_base64 = body.get("image_base64")
90
+ notes = body.get("notes", "")
91
+ product_spec = body.get("product_spec", "")
92
  if not image_base64:
93
  return JSONResponse({"error": "image_base64 required"}, status_code=400)
94
 
95
  agents = get_agents()
 
96
 
97
+ # Run pipeline
98
+ result = await agents.run_pipeline(image_base64, notes=notes, product_spec=product_spec)
99
+
100
+ # Save to DB - ensure we include everything the frontend expects
101
  inspection_data = {
102
+ **result,
103
  "timestamp": datetime.now(timezone.utc).isoformat(),
104
+ "image_url": f"data:image/jpeg;base64,{image_base64}" if "," not in image_base64 else image_base64,
105
+ "notes": notes,
106
+ "product_spec": product_spec
 
 
107
  }
108
 
109
+ # Generate social post (using the reporter summary as the body)
110
+ try:
111
+ social = await agents.generate_social_post(
112
+ inspection_data.get("headline", "New Inspection"),
113
+ inspection_data.get("summary", "Complete analysis of project infrastructure.")
114
+ )
115
+ inspection_data["social"] = social
116
+ except:
117
+ inspection_data["social"] = {"x_post": "", "linkedin_post": ""}
118
+
119
  col, _ = await get_db_collections()
120
  if col is not None:
121
  await col.insert_one(inspection_data.copy())