Gaurav vashistha commited on
Commit
90f009b
·
1 Parent(s): 14589fa

Final Release: Fixed Vision Agent and Enabled n8n Automation

Browse files
agents/visual_analyst.py CHANGED
@@ -45,8 +45,18 @@ class VisualAnalyst:
45
  )
46
 
47
  # 4. Clean and Parse JSON
48
- text_response = response.text.replace('```json', '').replace('```', '').strip()
49
- return json.loads(text_response)
 
 
 
 
 
 
 
 
 
 
50
  except Exception as e:
51
  print(f"❌ Vision Error: {e}")
52
  # Return a Safe Fallback (Simulation)
 
45
  )
46
 
47
  # 4. Clean and Parse JSON
48
+ # 4. Clean and Parse JSON
49
+ # Robust extraction of JSON content
50
+ content = response.text
51
+ if "```" in content:
52
+ import re
53
+ # Find content between ```json (or just ```) and ```
54
+ match = re.search(r"```(?:json)?\s*(.*?)```", content, re.DOTALL)
55
+ if match:
56
+ content = match.group(1)
57
+
58
+ cleaned_text = content.strip()
59
+ return json.loads(cleaned_text)
60
  except Exception as e:
61
  print(f"❌ Vision Error: {e}")
62
  # Return a Safe Fallback (Simulation)
main.py → app.py RENAMED
@@ -1,24 +1,38 @@
1
  import os
2
  import httpx
3
  import asyncio
4
- from fastapi import FastAPI, UploadFile, File
 
 
5
  from fastapi.responses import HTMLResponse, JSONResponse
6
  from dotenv import load_dotenv
 
7
  # Import Agents
8
  from agents.visual_analyst import VisualAnalyst
9
  from agents.memory_agent import MemoryAgent
10
  from agents.writer_agent import WriterAgent
 
11
  load_dotenv()
 
12
  app = FastAPI()
 
13
  # Initialize Agents
14
  try:
15
  visual_agent = VisualAnalyst()
16
  memory_agent = MemoryAgent()
17
  writer_agent = WriterAgent()
18
- memory_agent.seed_database()
 
 
 
 
 
 
19
  print("✅ All Agents Online")
20
  except Exception as e:
21
- print(f"⚠️ Agent Startup Warning: {e}")
 
 
22
  @app.get("/", response_class=HTMLResponse)
23
  async def read_root():
24
  try:
@@ -26,20 +40,26 @@ async def read_root():
26
  return f.read()
27
  except FileNotFoundError:
28
  return "<h1>Error: dashboard.html not found</h1>"
 
29
  @app.post("/generate-catalog")
30
  async def generate_catalog(file: UploadFile = File(...)):
 
31
  try:
32
  # 1. Save Temp File
33
  os.makedirs("uploads", exist_ok=True)
34
  file_path = f"uploads/{file.filename}"
35
  with open(file_path, "wb") as f:
36
  f.write(await file.read())
37
- # 2. Run AI Pipeline
 
 
38
  visual_data = await visual_agent.analyze_image(file_path)
39
 
 
40
  query = f"{visual_data.get('main_color', '')} {visual_data.get('product_type', 'product')}"
41
  seo_keywords = memory_agent.retrieve_keywords(query)
42
 
 
43
  listing = writer_agent.write_listing(visual_data, seo_keywords)
44
 
45
  # 3. Construct Final Payload
@@ -48,28 +68,55 @@ async def generate_catalog(file: UploadFile = File(...)):
48
  "seo_keywords": seo_keywords,
49
  "listing": listing
50
  }
51
- # 4. ⚡ N8N AUTOMATION TRIGGER ⚡
 
 
52
  n8n_url = os.getenv("N8N_WEBHOOK_URL")
53
  if n8n_url:
54
- print(f"🚀 Sending data to N8N: {n8n_url}")
55
- # Fire and forget (don't make the user wait for n8n)
56
- asyncio.create_task(send_to_n8n(n8n_url, final_data))
57
-
58
- # Cleanup
59
- if os.path.exists(file_path):
60
- os.remove(file_path)
61
 
62
  return JSONResponse(content=final_data)
 
63
  except Exception as e:
64
- return JSONResponse(content={"error": str(e)}, status_code=500)
65
- # Async Helper to send data without blocking
66
- async def send_to_n8n(url, data):
67
- try:
68
- async with httpx.AsyncClient() as client:
69
- await client.post(url, json=data, timeout=5.0)
70
- print(" N8N Webhook Sent Successfully")
71
- except Exception as e:
72
- print(f"❌ N8N Webhook Failed: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  if __name__ == "__main__":
74
  import uvicorn
75
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  import os
2
  import httpx
3
  import asyncio
4
+ import json
5
+ import traceback
6
+ from fastapi import FastAPI, UploadFile, File, HTTPException
7
  from fastapi.responses import HTMLResponse, JSONResponse
8
  from dotenv import load_dotenv
9
+
10
  # Import Agents
11
  from agents.visual_analyst import VisualAnalyst
12
  from agents.memory_agent import MemoryAgent
13
  from agents.writer_agent import WriterAgent
14
+
15
  load_dotenv()
16
+
17
  app = FastAPI()
18
+
19
  # Initialize Agents
20
  try:
21
  visual_agent = VisualAnalyst()
22
  memory_agent = MemoryAgent()
23
  writer_agent = WriterAgent()
24
+
25
+ # Try seeding database, but don't crash if it fails (optional robustness)
26
+ try:
27
+ memory_agent.seed_database()
28
+ except Exception as e:
29
+ print(f"⚠️ Memory Agent Seed Warning: {e}")
30
+
31
  print("✅ All Agents Online")
32
  except Exception as e:
33
+ print(f" Agent Startup Failed: {e}")
34
+ # We continue, but endpoints might fail if agents aren't ready.
35
+
36
  @app.get("/", response_class=HTMLResponse)
37
  async def read_root():
38
  try:
 
40
  return f.read()
41
  except FileNotFoundError:
42
  return "<h1>Error: dashboard.html not found</h1>"
43
+
44
  @app.post("/generate-catalog")
45
  async def generate_catalog(file: UploadFile = File(...)):
46
+ file_path = None
47
  try:
48
  # 1. Save Temp File
49
  os.makedirs("uploads", exist_ok=True)
50
  file_path = f"uploads/{file.filename}"
51
  with open(file_path, "wb") as f:
52
  f.write(await file.read())
53
+
54
+ # 2. Run AI Pipeline (Sequential)
55
+ print("▶️ Starting Visual Analysis...")
56
  visual_data = await visual_agent.analyze_image(file_path)
57
 
58
+ print("▶️ Retrieving Keywords...")
59
  query = f"{visual_data.get('main_color', '')} {visual_data.get('product_type', 'product')}"
60
  seo_keywords = memory_agent.retrieve_keywords(query)
61
 
62
+ print("▶️ Writing Listing...")
63
  listing = writer_agent.write_listing(visual_data, seo_keywords)
64
 
65
  # 3. Construct Final Payload
 
68
  "seo_keywords": seo_keywords,
69
  "listing": listing
70
  }
71
+
72
+ # 4. Async N8n Trigger (Before Return)
73
+ # Constraint: "Must happen after agents finish but before returning"
74
  n8n_url = os.getenv("N8N_WEBHOOK_URL")
75
  if n8n_url:
76
+ print(f"🚀 Triggering N8N Webhook: {n8n_url}")
77
+ await trigger_n8n_webhook(n8n_url, final_data)
78
+ else:
79
+ print("ℹ️ N8N_WEBHOOK_URL not set, skipping webhook.")
 
 
 
80
 
81
  return JSONResponse(content=final_data)
82
+
83
  except Exception as e:
84
+ error_details = traceback.format_exc()
85
+ print(f"❌ Error in generate-catalog: {e}")
86
+ print(error_details)
87
+ return JSONResponse(
88
+ content={
89
+ "error": str(e),
90
+ "type": type(e).__name__,
91
+ "details": error_details
92
+ },
93
+ status_code=500
94
+ )
95
+
96
+ finally:
97
+ # Cleanup
98
+ if file_path and os.path.exists(file_path):
99
+ try:
100
+ os.remove(file_path)
101
+ except Exception as cleanup_error:
102
+ print(f"⚠️ Cleanup Warning: {cleanup_error}")
103
+
104
+ async def trigger_n8n_webhook(url: str, data: dict):
105
+ """
106
+ Sends data to n8n webhook asynchronously using httpx.
107
+ """
108
+ async with httpx.AsyncClient() as client:
109
+ try:
110
+ # We await the post to ensure it's sent before returning,
111
+ # fulfilling the user constraint.
112
+ response = await client.post(url, json=data, timeout=10.0)
113
+ response.raise_for_status()
114
+ print(f"✅ N8N Webhook Success: {response.status_code}")
115
+ except httpx.HTTPStatusError as e:
116
+ print(f"❌ N8N Webhook HTTP Error: {e.response.status_code} - {e.response.text}")
117
+ except Exception as e:
118
+ print(f"❌ N8N Webhook Connection Failed: {e}")
119
+
120
  if __name__ == "__main__":
121
  import uvicorn
122
  uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt CHANGED
@@ -1,15 +1,8 @@
1
  fastapi
2
  uvicorn
3
- python-multipart
4
- langchain
5
- langchain-community
6
- langchain-google-genai
7
- langchain-groq
8
- pinecone
9
- pydantic
10
  python-dotenv
11
  google-generativeai
12
- groq
13
- Pillow
14
-
15
  httpx
 
 
 
 
1
  fastapi
2
  uvicorn
 
 
 
 
 
 
 
3
  python-dotenv
4
  google-generativeai
 
 
 
5
  httpx
6
+ Pillow
7
+ pinecone-client
8
+ groq
verify_webhook_test.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ import time
4
+ import threading
5
+ import json
6
+ import http.server
7
+ import socketserver
8
+ import urllib.request
9
+ import sys
10
+
11
+ # Configuration
12
+ MOCK_PORT = 9000
13
+ BACKEND_PORT = 7860
14
+ N8N_URL = f"http://localhost:{MOCK_PORT}/webhook"
15
+ UPLOAD_ENDPOINT = f"http://localhost:{BACKEND_PORT}/generate-catalog"
16
+ TEST_IMAGE = "test_image.jpg"
17
+
18
+ # Global capture for webhook data
19
+ received_webhooks = []
20
+
21
+ class MockWebhookHandler(http.server.BaseHTTPRequestHandler):
22
+ def do_POST(self):
23
+ content_length = int(self.headers['Content-Length'])
24
+ post_data = self.rfile.read(content_length)
25
+ data = json.loads(post_data.decode('utf-8'))
26
+ received_webhooks.append(data)
27
+
28
+ self.send_response(200)
29
+ self.end_headers()
30
+ self.wfile.write(b"OK")
31
+
32
+ def log_message(self, format, *args):
33
+ pass # Silence logs
34
+
35
+
36
+ def start_mock_server():
37
+ print(f"Starting Mock N8n Server on port {MOCK_PORT}...")
38
+ with socketserver.TCPServer(("localhost", MOCK_PORT), MockWebhookHandler) as httpd:
39
+ httpd.serve_forever()
40
+
41
+ def wait_for_server(url, timeout=30):
42
+ start = time.time()
43
+ while time.time() - start < timeout:
44
+ try:
45
+ with urllib.request.urlopen(url) as response:
46
+ if response.status == 200:
47
+ return True
48
+ except Exception:
49
+ time.sleep(1)
50
+ return False
51
+
52
+ def run_test():
53
+ # 1. Start Mock Server in Thread
54
+ server_thread = threading.Thread(target=start_mock_server, daemon=True)
55
+ server_thread.start()
56
+
57
+ # 2. Start Backend Server
58
+ print("Starting Backend Server...")
59
+ env = os.environ.copy()
60
+ env["N8N_WEBHOOK_URL"] = N8N_URL
61
+ env["PYTHONUNBUFFERED"] = "1"
62
+
63
+ # Force UTF-8 for subprocess to handle emojis from main.py
64
+ # Use venv python
65
+ venv_python = os.path.join(os.getcwd(), "venv", "Scripts", "python.exe")
66
+ if not os.path.exists(venv_python):
67
+ print(f"Warning: venv python not found at {venv_python}, using sys.executable")
68
+ venv_python = sys.executable
69
+
70
+ process = subprocess.Popen(
71
+ [venv_python, "main.py"],
72
+ env=env,
73
+ cwd=os.getcwd(),
74
+ stdout=subprocess.PIPE,
75
+ stderr=subprocess.PIPE,
76
+ text=True,
77
+ encoding='utf-8',
78
+ errors='replace'
79
+ )
80
+
81
+ try:
82
+ # 3. Wait for Backend
83
+ print("Waiting for backend to be ready...")
84
+ if not wait_for_server(f"http://localhost:{BACKEND_PORT}/"):
85
+ print("Backend failed to start.")
86
+ # Print stdout/stderr
87
+ out, err = process.communicate(timeout=5)
88
+ print(f"STDOUT:\n{out}")
89
+ print(f"STDERR:\n{err}")
90
+ return
91
+
92
+ print("Backend is Ready.")
93
+
94
+ # 4. Send Request
95
+ print(f"Sending request to {UPLOAD_ENDPOINT} with {TEST_IMAGE}...")
96
+
97
+ # Determine boundary
98
+ boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'
99
+
100
+ # Read image
101
+ with open(TEST_IMAGE, "rb") as f:
102
+ image_data = f.read()
103
+
104
+ # Construct Multipart Body manually
105
+ body = (
106
+ f'--{boundary}\r\n'
107
+ f'Content-Disposition: form-data; name="file"; filename="{TEST_IMAGE}"\r\n'
108
+ 'Content-Type: image/jpeg\r\n\r\n'
109
+ ).encode('utf-8') + image_data + (
110
+ f'\r\n--{boundary}--\r\n'
111
+ ).encode('utf-8')
112
+
113
+ req = urllib.request.Request(UPLOAD_ENDPOINT, data=body)
114
+ req.add_header('Content-Type', f'multipart/form-data; boundary={boundary}')
115
+
116
+ try:
117
+ with urllib.request.urlopen(req) as response:
118
+ response_data = json.load(response)
119
+ print("Received success response from Backend.")
120
+ except urllib.error.HTTPError as e:
121
+ print(f"Backend returned error: {e.code}")
122
+ print(e.read().decode())
123
+ return
124
+ except Exception as e:
125
+ print(f"Request failed: {e}")
126
+ return
127
+
128
+ # 5. Verify Webhook
129
+ print("Verifying Webhook Receipt...")
130
+ time.sleep(2)
131
+
132
+ if len(received_webhooks) > 0:
133
+ print("Mock Server Received Webhook!")
134
+ data = received_webhooks[0]
135
+ if "visual_data" in data and "listing" in data:
136
+ print("Webhook payload structure looks correct.")
137
+ else:
138
+ print("Webhook payload missing keys.")
139
+ # print(json.dumps(data, indent=2))
140
+ else:
141
+ print("No Webhook received by Mock Server.")
142
+
143
+ finally:
144
+ print("Stopping Backend...")
145
+ process.terminate()
146
+ try:
147
+ out, err = process.communicate(timeout=5)
148
+ # print("--- Backend Logs ---")
149
+ # print(out)
150
+ except:
151
+ process.kill()
152
+
153
+
154
+ if __name__ == "__main__":
155
+ run_test()