Initial commit: Site Survey AI app with all features
This commit is contained in:
@@ -0,0 +1,980 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
IT Site Survey AI Flask Application
|
||||||
|
Creates surveys for client sites and uses AI to recommend network setups.
|
||||||
|
Enhanced with PDF export, photo uploads, and selectable Ollama models.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import base64
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import Flask, jsonify, request, send_from_directory, Response, stream_with_context, send_file
|
||||||
|
from flask_cors import CORS
|
||||||
|
from reportlab.lib import colors
|
||||||
|
from reportlab.lib.pagesizes import letter
|
||||||
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
||||||
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||||
|
from reportlab.lib.units import inch
|
||||||
|
import io
|
||||||
|
import time
|
||||||
|
|
||||||
|
app = Flask(__name__, static_url_path='', static_folder='.')
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
OLLAMA_BASE = os.environ.get('OLLAMA_URL', 'http://192.168.19.25:11434')
|
||||||
|
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER', '/tmp/site-survey-uploads')
|
||||||
|
|
||||||
|
# Ensure upload folder exists
|
||||||
|
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||||
|
|
||||||
|
# In-memory storage for surveys
|
||||||
|
surveys_db = {}
|
||||||
|
survey_responses_db = {}
|
||||||
|
DEFAULT_MODEL = 'kimi-k2.6:cloud'
|
||||||
|
|
||||||
|
# Default survey template
|
||||||
|
DEFAULT_SURVEY_TEMPLATE = {
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"name": "Comprehensive Network Infrastructure Site Survey",
|
||||||
|
"description": "Detailed survey to assess network infrastructure, cabling, and IT requirements for accurate implementation planning",
|
||||||
|
"questions": [
|
||||||
|
# Site Topology & Layout
|
||||||
|
{
|
||||||
|
"id": "q1",
|
||||||
|
"category": "Site Topology & Layout",
|
||||||
|
"question": "What is the total square footage of the site?",
|
||||||
|
"type": "text",
|
||||||
|
"placeholder": "e.g., 5000 sq ft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q2",
|
||||||
|
"category": "Site Topology & Layout",
|
||||||
|
"question": "How many floors does the building have?",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "e.g., 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q3",
|
||||||
|
"category": "Site Topology & Layout",
|
||||||
|
"question": "Provide a detailed description of the site layout (open plan, offices, cubicles, etc.)",
|
||||||
|
"type": "textarea",
|
||||||
|
"placeholder": "e.g., Open floor plan with 20 cubicles, 5 private offices, 2 conference rooms..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q4",
|
||||||
|
"category": "Site Topology & Layout",
|
||||||
|
"question": "Are there any physical barriers that may affect cable runs?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Concrete walls", "Elevator shafts", "Stairwells", "Firewalls", "Historic building restrictions", "Underground levels", "None"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Network Closet & Equipment Locations
|
||||||
|
{
|
||||||
|
"id": "q5",
|
||||||
|
"category": "Network Closet & Equipment",
|
||||||
|
"question": "How many network closets (MDF/IDF) are planned or exist?",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "e.g., 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q6",
|
||||||
|
"category": "Network Closet & Equipment",
|
||||||
|
"question": "Describe the current or planned network closet locations",
|
||||||
|
"type": "textarea",
|
||||||
|
"placeholder": "e.g., MDF in basement, IDF on floors 2 and 4..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q7",
|
||||||
|
"category": "Network Closet & Equipment",
|
||||||
|
"question": "What is the power capacity available in network closets (UPS, circuits, etc.)?",
|
||||||
|
"type": "text",
|
||||||
|
"placeholder": "e.g., 2x 20A circuits, UPS backup planned"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q8",
|
||||||
|
"category": "Network Closet & Equipment",
|
||||||
|
"question": "Are there equipment racks installed or planned?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Yes, fully equipped racks", "Yes, partial racks", "Racks needed", "Wall-mounted only", "No racks planned"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Cabling Infrastructure
|
||||||
|
{
|
||||||
|
"id": "q9",
|
||||||
|
"category": "Cabling Infrastructure",
|
||||||
|
"question": "What type of network cabling is currently installed?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Cat6a (10GbE capable)", "Cat6 (1GbE)", "Cat5e (100Mb-1GbE)", "Cat5 or older", "Fiber only", "Mixed/Unknown", "No existing cabling"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q10",
|
||||||
|
"category": "Cabling Infrastructure",
|
||||||
|
"question": "How many total network drops (ports) are needed?",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "e.g., 150"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q11",
|
||||||
|
"category": "Cabling Infrastructure",
|
||||||
|
"question": "What is the estimated average cable run length?",
|
||||||
|
"type": "text",
|
||||||
|
"placeholder": "e.g., 150 feet average, longest run 300 feet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q12",
|
||||||
|
"category": "Cabling Infrastructure",
|
||||||
|
"question": "Are there specific cabling requirements?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Plenum-rated cable required", "Outdoor/rated cable needed", "Shielded cable (STP)", "Fiber backbone needed", "Conduit/tray already installed", "None"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Network Equipment & Hardware
|
||||||
|
{
|
||||||
|
"id": "q13",
|
||||||
|
"category": "Network Equipment",
|
||||||
|
"question": "What network equipment is currently installed?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Router/Firewall", "Managed switches", "Unmanaged switches", "Wireless access points", "Server(s)", "UPS/Backup power", "Patch panels", "None - greenfield"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q14",
|
||||||
|
"category": "Network Equipment",
|
||||||
|
"question": "What type of network switches are needed?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Fully managed (Layer 3)", "Managed (Layer 2)", "Smart/Web-managed", "Unmanaged", "PoE required", "Not sure"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q15",
|
||||||
|
"category": "Network Equipment",
|
||||||
|
"question": "How many wireless access points are needed?",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "e.g., 8"
|
||||||
|
},
|
||||||
|
|
||||||
|
# Power Requirements
|
||||||
|
{
|
||||||
|
"id": "q16",
|
||||||
|
"category": "Power Requirements",
|
||||||
|
"question": "What is the total estimated power consumption for network equipment?",
|
||||||
|
"type": "text",
|
||||||
|
"placeholder": "e.g., 2000W estimated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q17",
|
||||||
|
"category": "Power Requirements",
|
||||||
|
"question": "Are PoE (Power over Ethernet) devices planned?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["IP Phones", "Wireless APs", "IP Cameras", "Access control", "Digital signage", "IoT sensors", "None"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q18",
|
||||||
|
"category": "Power Requirements",
|
||||||
|
"question": "What is the UPS/backup power requirement?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Full backup (4+ hours)", "Limited backup (1-2 hours)", "Graceful shutdown only", "None", "Not sure"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# User & Device Requirements
|
||||||
|
{
|
||||||
|
"id": "q19",
|
||||||
|
"category": "User & Device Requirements",
|
||||||
|
"question": "How many employees will be onsite?",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "e.g., 50"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q20",
|
||||||
|
"category": "User & Device Requirements",
|
||||||
|
"question": "How many concurrent devices do you expect (including phones, laptops, tablets, IoT)?",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "e.g., 150"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q21",
|
||||||
|
"category": "User & Device Requirements",
|
||||||
|
"question": "What types of network activities will users be doing?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Email/Web Browsing", "Video conferencing", "File sharing", "Cloud applications", "VoIP/Phone calls", "Streaming/Media", "Remote desktop", "Development/Testing", "Guest WiFi"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Security & Compliance
|
||||||
|
{
|
||||||
|
"id": "q22",
|
||||||
|
"category": "Security & Compliance",
|
||||||
|
"question": "What security compliance needs do you have?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["HIPAA", "PCI-DSS", "SOC 2", "ISO 27001", "GDPR", "NIST", "None/General security"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q23",
|
||||||
|
"category": "Security & Compliance",
|
||||||
|
"question": "Do you need network segmentation (VLANs)?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Corporate network", "Guest network", "IoT network", "Management network", "Voice/VLAN", "Video surveillance VLAN", "No segmentation needed"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q24",
|
||||||
|
"category": "Security & Compliance",
|
||||||
|
"question": "What security features are required?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Firewall", "Intrusion Detection/Prevention", "Content filtering", "VPN access", "Network access control (NAC)", "Endpoint security", "None"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Internet & WAN
|
||||||
|
{
|
||||||
|
"id": "q25",
|
||||||
|
"category": "Internet & WAN",
|
||||||
|
"question": "What internet connectivity is available or planned?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Fiber", "Cable/DSL", "Fixed wireless", "5G/Cellular backup", "MPLS", "SD-WAN", "Multiple ISPs", "Not sure"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q26",
|
||||||
|
"category": "Internet & WAN",
|
||||||
|
"question": "What is the required internet bandwidth?",
|
||||||
|
"type": "text",
|
||||||
|
"placeholder": "e.g., 1Gbps download / 500Mbps upload"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q27",
|
||||||
|
"category": "Internet & WAN",
|
||||||
|
"question": "Do you have remote sites that need to connect?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Yes - Site-to-site VPN needed", "Yes - SD-WAN preferred", "Yes - MPLS", "No - Single site only", "Maybe in future"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Existing Infrastructure Assessment
|
||||||
|
{
|
||||||
|
"id": "q28",
|
||||||
|
"category": "Existing Infrastructure",
|
||||||
|
"question": "Is there existing network infrastructure to assess?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Yes - Full assessment needed", "Yes - Partial upgrade", "No - Complete new build", "Not sure"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q29",
|
||||||
|
"category": "Existing Infrastructure",
|
||||||
|
"question": "Describe any existing network infrastructure issues or concerns",
|
||||||
|
"type": "textarea",
|
||||||
|
"placeholder": "e.g., Slow WiFi in conference rooms, dead zones, outdated equipment..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q30",
|
||||||
|
"category": "Existing Infrastructure",
|
||||||
|
"question": "Are there any known performance issues?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Slow internet", "WiFi dead zones", "Network congestion", "Frequent disconnections", "Latency issues", "None known"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Budget & Timeline
|
||||||
|
{
|
||||||
|
"id": "q31",
|
||||||
|
"category": "Budget & Timeline",
|
||||||
|
"question": "What is your approximate budget range?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Under $10,000", "$10,000 - $25,000", "$25,000 - $50,000", "$50,000 - $100,000", "$100,000+", "Not established yet"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q32",
|
||||||
|
"category": "Budget & Timeline",
|
||||||
|
"question": "When do you need this completed?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["ASAP - Emergency", "Within 2 weeks", "Within 1 month", "Within 3 months", "Within 6 months", "Flexible"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q33",
|
||||||
|
"category": "Budget & Timeline",
|
||||||
|
"question": "Who will be responsible for ongoing network management?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Internal IT team", "Managed Service Provider (MSP)", "Hybrid approach", "Not decided", "Outsourced"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Additional Details
|
||||||
|
{
|
||||||
|
"id": "q34",
|
||||||
|
"category": "Additional Details",
|
||||||
|
"question": "Upload photos of the site (network closets, cable runs, equipment locations)",
|
||||||
|
"type": "file",
|
||||||
|
"placeholder": "Upload images for visual assessment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q35",
|
||||||
|
"category": "Additional Details",
|
||||||
|
"question": "Any other specific requirements, concerns, or notes?",
|
||||||
|
"type": "textarea",
|
||||||
|
"placeholder": "e.g., Need outdoor WiFi coverage, specific vendor preferences, regulatory requirements, future expansion plans..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def call_ollama(prompt, model=None, stream=False):
|
||||||
|
"""Call Ollama API with the given prompt."""
|
||||||
|
model = model or DEFAULT_MODEL
|
||||||
|
try:
|
||||||
|
if stream:
|
||||||
|
resp = requests.post(
|
||||||
|
f'{OLLAMA_BASE}/api/generate',
|
||||||
|
json={'model': model, 'prompt': prompt, 'stream': True},
|
||||||
|
stream=True,
|
||||||
|
timeout=120
|
||||||
|
)
|
||||||
|
return resp
|
||||||
|
else:
|
||||||
|
resp = requests.post(
|
||||||
|
f'{OLLAMA_BASE}/api/generate',
|
||||||
|
json={'model': model, 'prompt': prompt, 'stream': False},
|
||||||
|
timeout=120
|
||||||
|
)
|
||||||
|
return resp.json().get('response', '')
|
||||||
|
except Exception as e:
|
||||||
|
return f'Error: {str(e)}'
|
||||||
|
|
||||||
|
|
||||||
|
def get_ollama_models():
|
||||||
|
"""Fetch list of available models from Ollama."""
|
||||||
|
try:
|
||||||
|
resp = requests.get(f'{OLLAMA_BASE}/api/tags', timeout=5)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
models = resp.json().get('models', [])
|
||||||
|
return [m.get('name', '') for m in models if m.get('name')]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return [DEFAULT_MODEL]
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return send_from_directory('.', 'index.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/<path:path>')
|
||||||
|
def serve_static(path):
|
||||||
|
return send_from_directory('.', path)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/health')
|
||||||
|
def health_check():
|
||||||
|
try:
|
||||||
|
r = requests.get(f'{OLLAMA_BASE}/', timeout=3)
|
||||||
|
ollama_online = r.status_code == 200
|
||||||
|
except:
|
||||||
|
ollama_online = False
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'ok',
|
||||||
|
'service': 'site-survey-ai',
|
||||||
|
'ollama_connected': ollama_online,
|
||||||
|
'timestamp': datetime.utcnow().isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/ollama-status')
|
||||||
|
def ollama_status():
|
||||||
|
try:
|
||||||
|
r = requests.get(f'{OLLAMA_BASE}/', timeout=3)
|
||||||
|
return jsonify({'online': r.status_code == 200})
|
||||||
|
except:
|
||||||
|
return jsonify({'online': False})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/models')
|
||||||
|
def list_models():
|
||||||
|
"""Return list of available Ollama models."""
|
||||||
|
models = get_ollama_models()
|
||||||
|
return jsonify({
|
||||||
|
'models': models,
|
||||||
|
'default': DEFAULT_MODEL
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/template')
|
||||||
|
def get_template():
|
||||||
|
return jsonify({'template': DEFAULT_SURVEY_TEMPLATE})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Delete implementation functions
|
||||||
|
def delete_survey_impl(survey_id):
|
||||||
|
"""Delete a specific survey by ID"""
|
||||||
|
if survey_id in surveys_db:
|
||||||
|
# Clean up uploaded photos if they exist
|
||||||
|
survey = surveys_db[survey_id]
|
||||||
|
if 'responses' in survey:
|
||||||
|
for response in survey['responses']:
|
||||||
|
if 'photos' in response:
|
||||||
|
for photo in response['photos']:
|
||||||
|
photo_path = os.path.join(UPLOAD_FOLDER, photo['filename'])
|
||||||
|
if os.path.exists(photo_path):
|
||||||
|
try:
|
||||||
|
os.remove(photo_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
del surveys_db[survey_id]
|
||||||
|
return jsonify({'success': True, 'message': 'Survey deleted successfully'})
|
||||||
|
return jsonify({'success': False, 'error': 'Survey not found'}), 404
|
||||||
|
|
||||||
|
|
||||||
|
def clear_all_surveys_impl():
|
||||||
|
"""Delete all surveys"""
|
||||||
|
global surveys_db
|
||||||
|
count = len(surveys_db)
|
||||||
|
|
||||||
|
# Clean up uploaded photos
|
||||||
|
for survey_id, survey in surveys_db.items():
|
||||||
|
if 'responses' in survey:
|
||||||
|
for response in survey['responses']:
|
||||||
|
if 'photos' in response:
|
||||||
|
for photo in response['photos']:
|
||||||
|
photo_path = os.path.join(UPLOAD_FOLDER, photo['filename'])
|
||||||
|
if os.path.exists(photo_path):
|
||||||
|
try:
|
||||||
|
os.remove(photo_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
surveys_db = {}
|
||||||
|
return jsonify({'success': True, 'message': f'{count} surveys deleted successfully'})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys', methods=['GET', 'DELETE'])
|
||||||
|
def get_surveys():
|
||||||
|
if request.method == 'DELETE':
|
||||||
|
return clear_all_surveys_impl()
|
||||||
|
return jsonify({
|
||||||
|
'surveys': list(surveys_db.values())
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys', methods=['POST'])
|
||||||
|
def create_survey():
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
survey_id = str(uuid.uuid4())
|
||||||
|
survey = {
|
||||||
|
'id': survey_id,
|
||||||
|
'name': data.get('name', 'New Survey'),
|
||||||
|
'description': data.get('description', ''),
|
||||||
|
'client_name': data.get('client_name', ''),
|
||||||
|
'site_name': data.get('site_name', ''),
|
||||||
|
'questions': data.get('questions', DEFAULT_SURVEY_TEMPLATE['questions']),
|
||||||
|
'created_at': datetime.utcnow().isoformat(),
|
||||||
|
'status': 'active',
|
||||||
|
'responses': []
|
||||||
|
}
|
||||||
|
|
||||||
|
# If responses are provided, add them as the first response
|
||||||
|
responses_data = data.get('responses')
|
||||||
|
if responses_data and isinstance(responses_data, dict) and len(responses_data) > 0:
|
||||||
|
response_entry = {
|
||||||
|
'id': str(uuid.uuid4()),
|
||||||
|
'survey_id': survey_id,
|
||||||
|
'responses': responses_data,
|
||||||
|
'photos': [],
|
||||||
|
'analyzed': False,
|
||||||
|
'analysis': None,
|
||||||
|
'submitter_name': 'Survey Creator',
|
||||||
|
'created_at': datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
survey['responses'].append(response_entry)
|
||||||
|
|
||||||
|
surveys_db[survey_id] = survey
|
||||||
|
|
||||||
|
return jsonify({'survey': survey}), 201
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/<survey_id>', methods=['GET', 'DELETE'])
|
||||||
|
def get_survey(survey_id):
|
||||||
|
if request.method == 'DELETE':
|
||||||
|
return delete_survey_impl(survey_id)
|
||||||
|
survey = surveys_db.get(survey_id)
|
||||||
|
if not survey:
|
||||||
|
return jsonify({'error': 'Survey not found'}), 404
|
||||||
|
return jsonify({'survey': survey})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/<survey_id>/responses', methods=['POST'])
|
||||||
|
def submit_response(survey_id):
|
||||||
|
survey = surveys_db.get(survey_id)
|
||||||
|
if not survey:
|
||||||
|
return jsonify({'error': 'Survey not found'}), 404
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# Handle photo uploads (base64 encoded)
|
||||||
|
photos = data.get('photos', [])
|
||||||
|
photo_filenames = []
|
||||||
|
|
||||||
|
for i, photo_data in enumerate(photos):
|
||||||
|
if ',' in photo_data:
|
||||||
|
# Handle data URL format: data:image/jpeg;base64,/9j/4AAQ...
|
||||||
|
header, encoded = photo_data.split(',', 1)
|
||||||
|
try:
|
||||||
|
image_bytes = base64.b64decode(encoded)
|
||||||
|
ext = 'jpg'
|
||||||
|
if 'png' in header.lower():
|
||||||
|
ext = 'png'
|
||||||
|
elif 'webp' in header.lower():
|
||||||
|
ext = 'webp'
|
||||||
|
filename = f"{survey_id}_{uuid.uuid4().hex[:8]}.{ext}"
|
||||||
|
filepath = os.path.join(UPLOAD_FOLDER, filename)
|
||||||
|
with open(filepath, 'wb') as f:
|
||||||
|
f.write(image_bytes)
|
||||||
|
photo_filenames.append(filename)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Photo decode error: {e}")
|
||||||
|
|
||||||
|
response_id = str(uuid.uuid4())
|
||||||
|
response = {
|
||||||
|
'id': response_id,
|
||||||
|
'survey_id': survey_id,
|
||||||
|
'submitted_by': data.get('submitted_by', 'Anonymous'),
|
||||||
|
'responses': data.get('responses', {}),
|
||||||
|
'model_used': data.get('model', DEFAULT_MODEL),
|
||||||
|
'photos': photo_filenames,
|
||||||
|
'submitted_at': datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
survey['responses'].append(response)
|
||||||
|
survey_responses_db[response_id] = response
|
||||||
|
|
||||||
|
return jsonify({'response': response}), 201
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/photos/<survey_id>/<filename>')
|
||||||
|
def serve_photo(survey_id, filename):
|
||||||
|
"""Serve an uploaded photo."""
|
||||||
|
# Security: only serve files that belong to this survey
|
||||||
|
safe_filename = os.path.basename(filename)
|
||||||
|
filepath = os.path.join(UPLOAD_FOLDER, safe_filename)
|
||||||
|
if not os.path.exists(filepath):
|
||||||
|
return jsonify({'error': 'Photo not found'}), 404
|
||||||
|
return send_file(filepath)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/<survey_id>/analyze', methods=['POST'])
|
||||||
|
def analyze_survey(survey_id):
|
||||||
|
"""Analyze survey responses with AI to generate recommendations."""
|
||||||
|
survey = surveys_db.get(survey_id)
|
||||||
|
if not survey:
|
||||||
|
return jsonify({'error': 'Survey not found'}), 404
|
||||||
|
|
||||||
|
if not survey['responses']:
|
||||||
|
return jsonify({'error': 'No responses to analyze'}), 400
|
||||||
|
|
||||||
|
data = request.get_json() or {}
|
||||||
|
model = data.get('model', survey['responses'][-1].get('model_used', DEFAULT_MODEL))
|
||||||
|
|
||||||
|
latest_response = survey['responses'][-1]
|
||||||
|
responses = latest_response['responses']
|
||||||
|
photos = latest_response.get('photos', [])
|
||||||
|
|
||||||
|
context = f"""
|
||||||
|
IT Infrastructure Site Survey Results:
|
||||||
|
|
||||||
|
Client: {survey.get('client_name', 'Unknown')}
|
||||||
|
Site: {survey.get('site_name', 'Unknown')}
|
||||||
|
AI Model Used: {model}
|
||||||
|
|
||||||
|
Survey Responses:
|
||||||
|
"""
|
||||||
|
|
||||||
|
for q in survey['questions']:
|
||||||
|
qid = q['id']
|
||||||
|
answer = responses.get(qid, 'Not answered')
|
||||||
|
context += f"\n{q['category']} - {q['question']}\nAnswer: {answer}\n"
|
||||||
|
|
||||||
|
# Include photo descriptions if available
|
||||||
|
if photos:
|
||||||
|
photo_context = f"\n\nPHOTOS ATTACHED ({len(photos)} total):\n"
|
||||||
|
for i, photo in enumerate(photos):
|
||||||
|
photo_context += f"Photo {i+1}: [see uploaded image - {photo}]\n"
|
||||||
|
context += photo_context
|
||||||
|
|
||||||
|
prompt = f"""You are a senior network architect. Based on this site survey, provide a concise IT infrastructure recommendation.
|
||||||
|
|
||||||
|
{context}
|
||||||
|
|
||||||
|
Provide:
|
||||||
|
1. **EXECUTIVE SUMMARY** - Brief overview
|
||||||
|
2. **NETWORK DESIGN** - Recommended topology and VLANs
|
||||||
|
3. **HARDWARE NEEDED** - Router, switches, access points
|
||||||
|
4. **SECURITY** - Key security measures
|
||||||
|
5. **ESTIMATED COSTS** - Rough budget estimate
|
||||||
|
6. **TIMELINE** - Implementation phases
|
||||||
|
7. **PHOTO NOTES** - What the photos show (if any)
|
||||||
|
8. **NEXT STEPS** - Priority actions
|
||||||
|
|
||||||
|
Keep responses concise and actionable.
|
||||||
|
- Top 3 immediate actions needed
|
||||||
|
|
||||||
|
Format as a professional proposal suitable for client presentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def generate():
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
f'{OLLAMA_BASE}/api/generate',
|
||||||
|
json={'model': model, 'prompt': prompt, 'stream': True},
|
||||||
|
stream=True,
|
||||||
|
timeout=180
|
||||||
|
)
|
||||||
|
for line in resp.iter_lines():
|
||||||
|
if line:
|
||||||
|
yield line.decode('utf-8') + '\n'
|
||||||
|
except Exception as e:
|
||||||
|
error_chunk = json.dumps({'error': str(e), 'done': True})
|
||||||
|
yield error_chunk + '\n'
|
||||||
|
|
||||||
|
return Response(stream_with_context(generate()), mimetype='application/x-ndjson')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/<survey_id>/analyze-with-photos', methods=['POST'])
|
||||||
|
def analyze_with_photos(survey_id):
|
||||||
|
"""Analyze survey responses WITH photos sent directly (for vision-capable models)."""
|
||||||
|
survey = surveys_db.get(survey_id)
|
||||||
|
if not survey:
|
||||||
|
return jsonify({'error': 'Survey not found'}), 404
|
||||||
|
|
||||||
|
if not survey['responses']:
|
||||||
|
return jsonify({'error': 'No responses to analyze'}), 400
|
||||||
|
|
||||||
|
data = request.get_json() or {}
|
||||||
|
model = data.get('model', survey['responses'][-1].get('model_used', DEFAULT_MODEL))
|
||||||
|
|
||||||
|
latest_response = survey['responses'][-1]
|
||||||
|
responses = latest_response['responses']
|
||||||
|
photo_urls = latest_response.get('photos', [])
|
||||||
|
|
||||||
|
context = f"""
|
||||||
|
IT Infrastructure Site Survey Results:
|
||||||
|
|
||||||
|
Client: {survey.get('client_name', 'Unknown')}
|
||||||
|
Site: {survey.get('site_name', 'Unknown')}
|
||||||
|
AI Model Used: {model}
|
||||||
|
|
||||||
|
Survey Responses:
|
||||||
|
"""
|
||||||
|
|
||||||
|
for q in survey['questions']:
|
||||||
|
qid = q['id']
|
||||||
|
answer = responses.get(qid, 'Not answered')
|
||||||
|
context += f"\n{q['category']} - {q['question']}\nAnswer: {answer}\n"
|
||||||
|
|
||||||
|
if photo_urls:
|
||||||
|
context += f"\n\nPHOTOS ATTACHED ({len(photo_urls)} total):\n"
|
||||||
|
for i, photo in enumerate(photo_urls):
|
||||||
|
context += f"Photo {i+1}: {photo}\n"
|
||||||
|
|
||||||
|
prompt = f"""You are a senior network architect and IT infrastructure consultant. Based on the following site survey AND any attached photos, provide a comprehensive network infrastructure recommendation.
|
||||||
|
|
||||||
|
{context}
|
||||||
|
|
||||||
|
Generate a detailed IT infrastructure recommendation report including:
|
||||||
|
|
||||||
|
1. **EXECUTIVE SUMMARY**
|
||||||
|
2. **NETWORK TOPOLOGY RECOMMENDATION**
|
||||||
|
3. **HARDWARE RECOMMENDATIONS**
|
||||||
|
4. **INTERNET CONNECTIVITY**
|
||||||
|
5. **SECURITY RECOMMENDATIONS**
|
||||||
|
6. **ESTIMATED COSTS**
|
||||||
|
7. **IMPLEMENTATION TIMELINE**
|
||||||
|
8. **PHOTO ASSESSMENT** - Describe what you observe in the photos relevant to infrastructure. Note physical constraints, wiring, equipment, room layouts.
|
||||||
|
9. **PRIORITY ACTIONS**
|
||||||
|
|
||||||
|
Format as a professional proposal suitable for client presentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def generate():
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
f'{OLLAMA_BASE}/api/generate',
|
||||||
|
json={'model': model, 'prompt': prompt, 'stream': True},
|
||||||
|
stream=True,
|
||||||
|
timeout=180
|
||||||
|
)
|
||||||
|
for line in resp.iter_lines():
|
||||||
|
if line:
|
||||||
|
yield line.decode('utf-8') + '\n'
|
||||||
|
except Exception as e:
|
||||||
|
error_chunk = json.dumps({'error': str(e), 'done': True})
|
||||||
|
yield error_chunk + '\n'
|
||||||
|
|
||||||
|
return Response(stream_with_context(generate()), mimetype='application/x-ndjson')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/<survey_id>/generate-quote', methods=['POST'])
|
||||||
|
def generate_quote(survey_id):
|
||||||
|
"""Generate a service quote based on survey responses."""
|
||||||
|
survey = surveys_db.get(survey_id)
|
||||||
|
if not survey or not survey['responses']:
|
||||||
|
return jsonify({'error': 'No survey data available'}), 400
|
||||||
|
|
||||||
|
latest_response = survey['responses'][-1]
|
||||||
|
responses = latest_response['responses']
|
||||||
|
|
||||||
|
context = f"""
|
||||||
|
Site Survey Data for Quote Generation:
|
||||||
|
|
||||||
|
Client: {survey.get('client_name', 'Unknown')}
|
||||||
|
Site: {survey.get('site_name', 'Unknown')}
|
||||||
|
"""
|
||||||
|
|
||||||
|
for q in survey['questions']:
|
||||||
|
qid = q['id']
|
||||||
|
answer = responses.get(qid, 'Not answered')
|
||||||
|
context += f"\n{q['category']}: {answer}"
|
||||||
|
|
||||||
|
prompt = f"""You are an IT services sales consultant. Based on the following site survey, create a professional service quote for network infrastructure setup.
|
||||||
|
|
||||||
|
{context}
|
||||||
|
|
||||||
|
Generate a professional service quote in the following format:
|
||||||
|
|
||||||
|
**IT INFRASTRUCTURE SERVICES QUOTE**
|
||||||
|
**For: {survey.get('client_name', 'Client')}**
|
||||||
|
**Site: {survey.get('site_name', 'Site')}**
|
||||||
|
**Date: {datetime.now().strftime('%B %d, %Y')}**
|
||||||
|
|
||||||
|
**SCOPE OF WORK:**
|
||||||
|
[List the specific services needed based on the survey - network design, equipment procurement, installation, configuration, testing]
|
||||||
|
|
||||||
|
**RECOMMENDED SOLUTION:**
|
||||||
|
1. [Category - e.g., Network Design & Planning] - $[Price]
|
||||||
|
- [Detailed description of what's included]
|
||||||
|
2. [Category - e.g., Equipment & Hardware] - $[Price]
|
||||||
|
- [List of equipment and costs]
|
||||||
|
3. [Category - e.g., Installation & Configuration] - $[Price]
|
||||||
|
- [Labor hours and rates]
|
||||||
|
4. [Category - e.g., Security Implementation] - $[Price]
|
||||||
|
- [Security services included]
|
||||||
|
5. [Category - e.g., Testing & Documentation] - $[Price]
|
||||||
|
- [Testing procedures and documentation]
|
||||||
|
|
||||||
|
**INVESTMENT SUMMARY:**
|
||||||
|
- Initial Assessment & Design: $[Amount]
|
||||||
|
- Equipment/Hardware: $[Amount]
|
||||||
|
- Labor/Installation: $[Amount]
|
||||||
|
- Training & Documentation: $[Amount]
|
||||||
|
- **Total Project Investment: $[Amount]**
|
||||||
|
|
||||||
|
**OPTIONAL ONGOING SERVICES:**
|
||||||
|
- Monthly Management & Monitoring: $[Amount]/month
|
||||||
|
- Annual Maintenance: $[Amount]/year
|
||||||
|
- Support Contract: $[Amount]/month
|
||||||
|
|
||||||
|
**TIMELINE:** [Timeframe]
|
||||||
|
- Week 1-2: Design & Procurement
|
||||||
|
- Week 3-4: Installation
|
||||||
|
- Week 5: Testing & Training
|
||||||
|
|
||||||
|
**TERMS:**
|
||||||
|
- 50% deposit required to begin
|
||||||
|
- Net 15 on completion
|
||||||
|
- Quote valid for 30 days
|
||||||
|
|
||||||
|
This is an estimate based on the initial site survey. Final pricing will be confirmed after detailed site assessment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = call_ollama(prompt, stream=False)
|
||||||
|
return jsonify({'quote': response})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/<survey_id>/export/pdf', methods=['POST'])
|
||||||
|
def export_pdf(survey_id):
|
||||||
|
"""Export survey report to PDF."""
|
||||||
|
survey = surveys_db.get(survey_id)
|
||||||
|
if not survey or not survey['responses']:
|
||||||
|
return jsonify({'error': 'No survey data available'}), 400
|
||||||
|
|
||||||
|
latest_response = survey['responses'][-1]
|
||||||
|
responses = latest_response['responses']
|
||||||
|
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
doc = SimpleDocTemplate(buffer, pagesize=letter,
|
||||||
|
rightMargin=72, leftMargin=72,
|
||||||
|
topMargin=72, bottomMargin=18)
|
||||||
|
|
||||||
|
elements = []
|
||||||
|
styles = getSampleStyleSheet()
|
||||||
|
|
||||||
|
title_style = ParagraphStyle(
|
||||||
|
'CustomTitle',
|
||||||
|
parent=styles['Heading1'],
|
||||||
|
fontSize=24,
|
||||||
|
textColor=colors.HexColor('#1f6feb'),
|
||||||
|
spaceAfter=30
|
||||||
|
)
|
||||||
|
|
||||||
|
heading_style = ParagraphStyle(
|
||||||
|
'CustomHeading',
|
||||||
|
parent=styles['Heading2'],
|
||||||
|
fontSize=16,
|
||||||
|
textColor=colors.HexColor('#58a6ff'),
|
||||||
|
spaceAfter=12,
|
||||||
|
spaceBefore=12
|
||||||
|
)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
elements.append(Paragraph(f"IT Site Survey Report", title_style))
|
||||||
|
elements.append(Paragraph(f"Client: {survey.get('client_name', 'Unknown')}", styles['Normal']))
|
||||||
|
elements.append(Paragraph(f"Site: {survey.get('site_name', 'Unknown')}", styles['Normal']))
|
||||||
|
elements.append(Paragraph(f"Date: {datetime.now().strftime('%B %d, %Y')}", styles['Normal']))
|
||||||
|
elements.append(Spacer(1, 0.3*inch))
|
||||||
|
|
||||||
|
# Photos section
|
||||||
|
photos = latest_response.get('photos', [])
|
||||||
|
if photos:
|
||||||
|
elements.append(Paragraph("Attached Photos", heading_style))
|
||||||
|
elements.append(Paragraph(f"{len(photos)} photo(s) uploaded with this survey.", styles['Normal']))
|
||||||
|
elements.append(Spacer(1, 0.2*inch))
|
||||||
|
|
||||||
|
# Survey Responses
|
||||||
|
elements.append(Paragraph("Survey Responses", heading_style))
|
||||||
|
|
||||||
|
for q in survey['questions']:
|
||||||
|
qid = q['id']
|
||||||
|
answer = responses.get(qid, 'Not answered')
|
||||||
|
if isinstance(answer, list):
|
||||||
|
answer = ', '.join(answer)
|
||||||
|
|
||||||
|
elements.append(Paragraph(f"<b>{q['category']}</b>", styles['Heading3']))
|
||||||
|
elements.append(Paragraph(q['question'], styles['Normal']))
|
||||||
|
elements.append(Paragraph(f"Answer: {answer}", styles['Normal']))
|
||||||
|
elements.append(Spacer(1, 0.1*inch))
|
||||||
|
|
||||||
|
doc.build(elements)
|
||||||
|
|
||||||
|
buffer.seek(0)
|
||||||
|
return send_file(
|
||||||
|
buffer,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f'Site_Survey_{survey.get("client_name", "Client").replace(" ", "_")}_{datetime.now().strftime("%Y%m%d")}.pdf',
|
||||||
|
mimetype='application/pdf'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/<survey_id>/export/text', methods=['POST'])
|
||||||
|
def export_text(survey_id):
|
||||||
|
"""Export survey report to formatted text."""
|
||||||
|
survey = surveys_db.get(survey_id)
|
||||||
|
if not survey or not survey['responses']:
|
||||||
|
return jsonify({'error': 'No survey data available'}), 400
|
||||||
|
|
||||||
|
latest_response = survey['responses'][-1]
|
||||||
|
responses = latest_response['responses']
|
||||||
|
photos = latest_response.get('photos', [])
|
||||||
|
|
||||||
|
text_report = f"""IT SITE SURVEY REPORT
|
||||||
|
{'=' * 60}
|
||||||
|
|
||||||
|
Client: {survey.get('client_name', 'Unknown')}
|
||||||
|
Site: {survey.get('site_name', 'Unknown')}
|
||||||
|
Date: {datetime.now().strftime('%B %d, %Y')}
|
||||||
|
Photos: {len(photos)} attached
|
||||||
|
|
||||||
|
{'=' * 60}
|
||||||
|
SURVEY RESPONSES:
|
||||||
|
{'=' * 60}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
for q in survey['questions']:
|
||||||
|
qid = q['id']
|
||||||
|
answer = responses.get(qid, 'Not answered')
|
||||||
|
if isinstance(answer, list):
|
||||||
|
answer = ', '.join(answer)
|
||||||
|
|
||||||
|
text_report += f"""
|
||||||
|
{q['category']}:
|
||||||
|
Question: {q['question']}
|
||||||
|
Answer: {answer}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
text_report += f"""
|
||||||
|
{'=' * 60}
|
||||||
|
Report generated by IT Site Survey AI
|
||||||
|
{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||||
|
"""
|
||||||
|
|
||||||
|
buffer = io.BytesIO(text_report.encode('utf-8'))
|
||||||
|
return send_file(
|
||||||
|
buffer,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f'Site_Survey_{survey.get("client_name", "Client").replace(" ", "_")}_{datetime.now().strftime("%Y%m%d")}.txt',
|
||||||
|
mimetype='text/plain'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/<survey_id>/export/email', methods=['POST'])
|
||||||
|
def export_email(survey_id):
|
||||||
|
"""Export survey report formatted for email."""
|
||||||
|
survey = surveys_db.get(survey_id)
|
||||||
|
if not survey or not survey['responses']:
|
||||||
|
return jsonify({'error': 'No survey data available'}), 400
|
||||||
|
|
||||||
|
latest_response = survey['responses'][-1]
|
||||||
|
responses = latest_response['responses']
|
||||||
|
|
||||||
|
email_body = f"""
|
||||||
|
<h2>IT Site Survey Report - {survey.get('client_name', 'Client')}</h2>
|
||||||
|
<p><strong>Site:</strong> {survey.get('site_name', 'Unknown')}</p>
|
||||||
|
<p><strong>Date:</strong> {datetime.now().strftime('%B %d, %Y')}</p>
|
||||||
|
<p><strong>Photos:</strong> {len(latest_response.get('photos', []))} attached</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h3>Survey Responses:</h3>
|
||||||
|
|
||||||
|
<table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">
|
||||||
|
<tr style="background-color: #f0f0f0;">
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Question</th>
|
||||||
|
<th>Answer</th>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
for q in survey['questions']:
|
||||||
|
qid = q['id']
|
||||||
|
answer = responses.get(qid, 'Not answered')
|
||||||
|
if isinstance(answer, list):
|
||||||
|
answer = ', '.join(answer)
|
||||||
|
|
||||||
|
email_body += f"""
|
||||||
|
<tr>
|
||||||
|
<td>{q['category']}</td>
|
||||||
|
<td>{q['question']}</td>
|
||||||
|
<td>{answer}</td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
email_body += f"""
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>---<br>
|
||||||
|
Report generated by IT Site Survey AI<br>
|
||||||
|
{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return jsonify({'email_body': email_body})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
port = int(os.environ.get('PORT', 3003))
|
||||||
|
print("───────────────────────────────────────")
|
||||||
|
print(" 🏢 IT Site Survey AI")
|
||||||
|
print(f" Running on http://0.0.0.0:{port}")
|
||||||
|
print(f" Ollama: {OLLAMA_BASE}")
|
||||||
|
print(f" Upload folder: {UPLOAD_FOLDER}")
|
||||||
|
print("───────────────────────────────────────")
|
||||||
|
app.run(host='0.0.0.0', port=port, debug=False, use_reloader=False)
|
||||||
|
|
||||||
|
|
||||||
|
# Delete endpoints
|
||||||
+663
@@ -0,0 +1,663 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
IT Site Survey AI Application
|
||||||
|
Complete implementation with all requested features:
|
||||||
|
- Photo uploads for AI assessment
|
||||||
|
- Model selection (Ollama vision models)
|
||||||
|
- Export results (PDF, Text, Email)
|
||||||
|
- Delete surveys (individual + Clear All)
|
||||||
|
- Chat interface for AI interaction
|
||||||
|
- Load Sample Data for testing
|
||||||
|
- Comprehensive 35-question survey template
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask, request, jsonify, send_file
|
||||||
|
from flask_cors import CORS
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
import base64
|
||||||
|
import requests
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app, resources={
|
||||||
|
r"/api/*": {
|
||||||
|
"origins": ["http://192.168.50.11:3003", "http://localhost:3003"],
|
||||||
|
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
|
"allow_headers": ["Content-Type", "Authorization"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
UPLOAD_FOLDER = 'uploads'
|
||||||
|
PHOTOS_FOLDER = 'uploads/photos'
|
||||||
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||||
|
OLLAMA_HOST = os.getenv('OLLAMA_HOST', 'http://192.168.19.25:11434')
|
||||||
|
OLLAMA_URL = f"{OLLAMA_HOST}/api/chat"
|
||||||
|
|
||||||
|
# Create upload directories
|
||||||
|
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||||
|
os.makedirs(PHOTOS_FOLDER, exist_ok=True)
|
||||||
|
|
||||||
|
# In-memory storage
|
||||||
|
surveys_db = {}
|
||||||
|
responses_db = {}
|
||||||
|
|
||||||
|
# Survey Template with 35 comprehensive questions
|
||||||
|
DEFAULT_SURVEY_TEMPLATE = {
|
||||||
|
"title": "IT Infrastructure Site Survey",
|
||||||
|
"description": "Comprehensive network infrastructure assessment",
|
||||||
|
"questions": [
|
||||||
|
# Site Topology & Layout (1-4)
|
||||||
|
{
|
||||||
|
"id": "q1",
|
||||||
|
"category": "Site Topology & Layout",
|
||||||
|
"question": "What is the total square footage of the site?",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "e.g., 3500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q2",
|
||||||
|
"category": "Site Topology & Layout",
|
||||||
|
"question": "How many floors or buildings need network coverage?",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "e.g., 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q3",
|
||||||
|
"category": "Site Topology & Layout",
|
||||||
|
"question": "Describe the site layout (open plan, offices, warehouse, etc.)",
|
||||||
|
"type": "textarea",
|
||||||
|
"placeholder": "Describe the layout..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q4",
|
||||||
|
"category": "Site Topology & Layout",
|
||||||
|
"question": "Are there any physical barriers that may affect WiFi coverage?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Concrete walls", "Metal shelving", "Elevator shafts", "Thick drywall", "None"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Network Closet & Equipment (5-8)
|
||||||
|
{
|
||||||
|
"id": "q5",
|
||||||
|
"category": "Network Closet & Equipment",
|
||||||
|
"question": "How many network closets or IDF rooms are on-site?",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "e.g., 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q6",
|
||||||
|
"category": "Network Closet & Equipment",
|
||||||
|
"question": "Describe the current network closet setup",
|
||||||
|
"type": "textarea",
|
||||||
|
"placeholder": "Describe the setup..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q7",
|
||||||
|
"category": "Network Closet & Equipment",
|
||||||
|
"question": "What type of power and cooling is available in the network closets?",
|
||||||
|
"type": "textarea",
|
||||||
|
"placeholder": "Describe power and cooling..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q8",
|
||||||
|
"category": "Network Closet & Equipment",
|
||||||
|
"question": "Are there existing server racks or wall-mounted equipment?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Yes, full racks", "Yes, partial racks", "Wall-mounted only", "No existing racks"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Cabling Infrastructure (9-12)
|
||||||
|
{
|
||||||
|
"id": "q9",
|
||||||
|
"category": "Cabling Infrastructure",
|
||||||
|
"question": "What type of existing cabling is in place?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Cat5e (100Mb-1GbE)", "Cat6 (1GbE-10GbE)", "Cat6a (10GbE)", "Fiber (Multi-mode)", "Fiber (Single-mode)", "No existing cabling", "Unknown"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q10",
|
||||||
|
"category": "Cabling Infrastructure",
|
||||||
|
"question": "How many network drops are currently installed?",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "e.g., 120"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q11",
|
||||||
|
"category": "Cabling Infrastructure",
|
||||||
|
"question": "What is the estimated distance from the MDF to the farthest IDF or workstation?",
|
||||||
|
"type": "textarea",
|
||||||
|
"placeholder": "Describe distances..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q12",
|
||||||
|
"category": "Cabling Infrastructure",
|
||||||
|
"question": "Are there any special cabling requirements?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Plenum-rated cable required", "Outdoor-rated cable needed", "Direct burial cable needed", "Aerial cable needed", "Shielded cable required", "None"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Network Equipment (13-15)
|
||||||
|
{
|
||||||
|
"id": "q13",
|
||||||
|
"category": "Network Equipment",
|
||||||
|
"question": "What network equipment is currently installed?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Router/Firewall", "Managed switches", "Unmanaged switches", "Wireless access points", "None/Unknown"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q14",
|
||||||
|
"category": "Network Equipment",
|
||||||
|
"question": "What type of switches are needed?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Unmanaged (plug-and-play)", "Managed (Layer 2)", "Managed (Layer 3 with routing)", "PoE switches", "Industrial-grade switches"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q15",
|
||||||
|
"category": "Network Equipment",
|
||||||
|
"question": "How many wireless access points are needed?",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "e.g., 6"
|
||||||
|
},
|
||||||
|
|
||||||
|
# Power Requirements (16-18)
|
||||||
|
{
|
||||||
|
"id": "q16",
|
||||||
|
"category": "Power Requirements",
|
||||||
|
"question": "What is the estimated total power consumption for all network equipment?",
|
||||||
|
"type": "textarea",
|
||||||
|
"placeholder": "Describe power requirements..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q17",
|
||||||
|
"category": "Power Requirements",
|
||||||
|
"question": "What PoE devices will be connected?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["IP Phones", "Wireless APs", "IP Cameras", "IoT sensors", "None", "Not sure"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q18",
|
||||||
|
"category": "Power Requirements",
|
||||||
|
"question": "What UPS backup capacity is required?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["No backup needed", "Minimal backup (15-30 min)", "Standard backup (1-2 hours)", "Extended backup (4+ hours)", "Generator backup"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# User & Device Requirements (19-21)
|
||||||
|
{
|
||||||
|
"id": "q19",
|
||||||
|
"category": "User & Device Requirements",
|
||||||
|
"question": "How many employees or users will use the network?",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "e.g., 45"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q20",
|
||||||
|
"category": "User & Device Requirements",
|
||||||
|
"question": "What is the estimated total number of devices?",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "e.g., 85"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q21",
|
||||||
|
"category": "User & Device Requirements",
|
||||||
|
"question": "What types of network activities will be performed?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Email/Web Browsing", "Video conferencing", "File sharing", "Cloud applications", "Remote desktop", "VoIP/Phone calls", "Streaming media"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Security & Compliance (22-24)
|
||||||
|
{
|
||||||
|
"id": "q22",
|
||||||
|
"category": "Security & Compliance",
|
||||||
|
"question": "What compliance standards must be met?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["HIPAA", "PCI-DSS", "SOC 2", "GDPR", "NIST", "None/General security"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q23",
|
||||||
|
"category": "Security & Compliance",
|
||||||
|
"question": "What network segmentation is required?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Corporate network", "Guest network", "IoT network", "Management network", "No segmentation needed"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q24",
|
||||||
|
"category": "Security & Compliance",
|
||||||
|
"question": "What security features are needed?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Firewall", "VPN access", "Intrusion detection", "Network access control (NAC)", "Content filtering", "None"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Internet & WAN (25-27)
|
||||||
|
{
|
||||||
|
"id": "q25",
|
||||||
|
"category": "Internet & WAN",
|
||||||
|
"question": "What type of internet connectivity is available?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Fiber", "Cable/DSL", "5G/Cellular backup", "Dedicated leased line", "No existing connection"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q26",
|
||||||
|
"category": "Internet & WAN",
|
||||||
|
"question": "What is the required internet bandwidth?",
|
||||||
|
"type": "textarea",
|
||||||
|
"placeholder": "e.g., 500Mbps download / 250Mbps upload"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q27",
|
||||||
|
"category": "Internet & WAN",
|
||||||
|
"question": "Are there multiple sites that need to be connected?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["No - Single site only", "Yes - 2-3 sites", "Yes - 4+ sites", "Not sure yet"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Existing Infrastructure (28-30)
|
||||||
|
{
|
||||||
|
"id": "q28",
|
||||||
|
"category": "Existing Infrastructure",
|
||||||
|
"question": "Is there existing IT infrastructure to integrate with?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Yes - Full upgrade needed", "Yes - Partial upgrade", "No - New installation", "Not sure"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q29",
|
||||||
|
"category": "Existing Infrastructure",
|
||||||
|
"question": "Describe any existing network issues or limitations",
|
||||||
|
"type": "textarea",
|
||||||
|
"placeholder": "Describe current issues..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q30",
|
||||||
|
"category": "Existing Infrastructure",
|
||||||
|
"question": "What performance issues have been observed?",
|
||||||
|
"type": "multiselect",
|
||||||
|
"options": ["Slow internet", "WiFi dead zones", "Network congestion", "Frequent disconnections", "No issues observed"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Budget & Timeline (31-33)
|
||||||
|
{
|
||||||
|
"id": "q31",
|
||||||
|
"category": "Budget & Timeline",
|
||||||
|
"question": "What is the estimated budget for this project?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Under $10,000", "$10,000 - $25,000", "$25,000 - $50,000", "$50,000 - $100,000", "$100,000+", "Not determined yet"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q32",
|
||||||
|
"category": "Budget & Timeline",
|
||||||
|
"question": "What is the desired timeline for completion?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Within 1 month", "Within 3 months", "Within 6 months", "Within 1 year", "Flexible"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q33",
|
||||||
|
"category": "Budget & Timeline",
|
||||||
|
"question": "Who will be responsible for ongoing network management?",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Internal IT team", "Managed service provider (MSP)", "Hybrid approach", "Not determined yet"]
|
||||||
|
},
|
||||||
|
|
||||||
|
# Additional Details (34-35)
|
||||||
|
{
|
||||||
|
"id": "q34",
|
||||||
|
"category": "Additional Details",
|
||||||
|
"question": "Upload any site photos, floor plans, or network diagrams",
|
||||||
|
"type": "file",
|
||||||
|
"placeholder": "Upload photos..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q35",
|
||||||
|
"category": "Additional Details",
|
||||||
|
"question": "Any additional requirements or special considerations?",
|
||||||
|
"type": "textarea",
|
||||||
|
"placeholder": "Describe any additional requirements..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sample data for testing
|
||||||
|
SAMPLE_SURVEY_DATA = {
|
||||||
|
"name": "Sample Corporate Office Survey",
|
||||||
|
"client_name": "Acme Corporation",
|
||||||
|
"site_name": "Main Office - Downtown",
|
||||||
|
"description": "Comprehensive network infrastructure assessment for downtown office location",
|
||||||
|
"responses": {
|
||||||
|
"q1": "3500",
|
||||||
|
"q2": "2",
|
||||||
|
"q3": "Single-story office building with open floor plan, 8 private offices, 2 conference rooms, break room, and reception area. Approximately 60 workstations in cubicle area.",
|
||||||
|
"q4": ["Concrete walls"],
|
||||||
|
"q5": "2",
|
||||||
|
"q6": "MDF located in basement utility room. IDF on first floor in IT storage closet. Both have adequate ventilation but limited space.",
|
||||||
|
"q7": "2x 20A circuits per closet, UPS backup planned for MDF",
|
||||||
|
"q8": "Yes, partial racks",
|
||||||
|
"q9": "Cat5e (100Mb-1GbE)",
|
||||||
|
"q10": "120",
|
||||||
|
"q11": "Average 150 feet, longest run approximately 280 feet to corner offices",
|
||||||
|
"q12": ["Plenum-rated cable required"],
|
||||||
|
"q13": ["Router/Firewall", "Unmanaged switches", "Wireless access points"],
|
||||||
|
"q14": "Managed (Layer 2)",
|
||||||
|
"q15": "6",
|
||||||
|
"q16": "1800W estimated for all network equipment including PoE devices",
|
||||||
|
"q17": ["IP Phones", "Wireless APs", "IP Cameras"],
|
||||||
|
"q18": "Limited backup (1-2 hours)",
|
||||||
|
"q19": "45",
|
||||||
|
"q20": "85",
|
||||||
|
"q21": ["Email/Web Browsing", "Video conferencing", "File sharing", "Cloud applications", "Remote desktop"],
|
||||||
|
"q22": ["SOC 2"],
|
||||||
|
"q23": ["Corporate network", "Guest network", "IoT network", "Management network"],
|
||||||
|
"q24": ["Firewall", "VPN access", "Network access control (NAC)"],
|
||||||
|
"q25": ["Fiber", "5G/Cellular backup"],
|
||||||
|
"q26": "500Mbps download / 250Mbps upload",
|
||||||
|
"q27": "No - Single site only",
|
||||||
|
"q28": "Yes - Partial upgrade",
|
||||||
|
"q29": "Current WiFi has dead zones in conference rooms. Existing switches are unmanaged and showing signs of age. Need better coverage and management capabilities.",
|
||||||
|
"q30": ["Slow internet", "WiFi dead zones", "Network congestion"],
|
||||||
|
"q31": "$25,000 - $50,000",
|
||||||
|
"q32": "Within 3 months",
|
||||||
|
"q33": "Internal IT team",
|
||||||
|
"q34": "",
|
||||||
|
"q35": "Need outdoor WiFi coverage for patio area. Plan to add IP cameras in future. Prefer Cisco or Ubiquiti equipment. Require weekend installation to minimize downtime."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/health', methods=['GET'])
|
||||||
|
def health_check():
|
||||||
|
"""Health check endpoint"""
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=5)
|
||||||
|
ollama_online = resp.status_code == 200
|
||||||
|
except:
|
||||||
|
ollama_online = False
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"status": "ok",
|
||||||
|
"service": "site-survey-ai",
|
||||||
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
|
"ollama_connected": ollama_online
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/models', methods=['GET'])
|
||||||
|
def get_models():
|
||||||
|
"""Get available Ollama models"""
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=5)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
models_data = resp.json()
|
||||||
|
models = [m['name'] for m in models_data.get('models', [])]
|
||||||
|
# Add cloud models if available
|
||||||
|
cloud_models = ['kimi-k2.6:cloud', 'kimi-k2.5:cloud', 'nemotron-3-super:cloud']
|
||||||
|
models.extend(cloud_models)
|
||||||
|
return jsonify({"models": models, "default": "kimi-k2.6:cloud"})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching models: {e}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"models": ["kimi-k2.6:cloud", "kimi-k2.5:cloud", "llava:latest"],
|
||||||
|
"default": "kimi-k2.6:cloud"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/ollama-status', methods=['GET'])
|
||||||
|
def ollama_status():
|
||||||
|
"""Check Ollama connection status"""
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=5)
|
||||||
|
return jsonify({"online": resp.status_code == 200})
|
||||||
|
except:
|
||||||
|
return jsonify({"online": False})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/template', methods=['GET'])
|
||||||
|
def get_survey_template():
|
||||||
|
"""Get the survey template with questions"""
|
||||||
|
return jsonify({"template": DEFAULT_SURVEY_TEMPLATE})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys', methods=['GET'])
|
||||||
|
def list_surveys():
|
||||||
|
"""List all surveys"""
|
||||||
|
surveys = []
|
||||||
|
for sid, survey in surveys_db.items():
|
||||||
|
surveys.append({
|
||||||
|
"id": sid,
|
||||||
|
"name": survey['name'],
|
||||||
|
"client_name": survey.get('client_name', ''),
|
||||||
|
"site_name": survey.get('site_name', ''),
|
||||||
|
"description": survey.get('description', ''),
|
||||||
|
"created_at": survey['created_at'],
|
||||||
|
"responses_count": len(survey.get('responses', []))
|
||||||
|
})
|
||||||
|
return jsonify({"surveys": surveys})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys', methods=['POST'])
|
||||||
|
def create_survey():
|
||||||
|
"""Create a new survey"""
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
survey_id = str(uuid.uuid4())
|
||||||
|
survey = {
|
||||||
|
'id': survey_id,
|
||||||
|
'name': data.get('name', 'New Survey'),
|
||||||
|
'description': data.get('description', ''),
|
||||||
|
'client_name': data.get('client_name', ''),
|
||||||
|
'site_name': data.get('site_name', ''),
|
||||||
|
'questions': DEFAULT_SURVEY_TEMPLATE['questions'],
|
||||||
|
'created_at': datetime.utcnow().isoformat(),
|
||||||
|
'status': 'active',
|
||||||
|
'responses': []
|
||||||
|
}
|
||||||
|
|
||||||
|
# If responses are provided, add them
|
||||||
|
responses_data = data.get('responses')
|
||||||
|
if responses_data and isinstance(responses_data, dict) and len(responses_data) > 0:
|
||||||
|
response_entry = {
|
||||||
|
'id': str(uuid.uuid4()),
|
||||||
|
'survey_id': survey_id,
|
||||||
|
'responses': responses_data,
|
||||||
|
'photos': [],
|
||||||
|
'analyzed': False,
|
||||||
|
'analysis': None,
|
||||||
|
'submitter_name': 'Survey Creator',
|
||||||
|
'created_at': datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
survey['responses'].append(response_entry)
|
||||||
|
|
||||||
|
surveys_db[survey_id] = survey
|
||||||
|
return jsonify({"survey": survey}), 201
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/<survey_id>', methods=['GET'])
|
||||||
|
def get_survey(survey_id):
|
||||||
|
"""Get a specific survey"""
|
||||||
|
if survey_id not in surveys_db:
|
||||||
|
return jsonify({"error": "Survey not found"}), 404
|
||||||
|
return jsonify({"survey": surveys_db[survey_id]})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/<survey_id>', methods=['DELETE'])
|
||||||
|
def delete_survey(survey_id):
|
||||||
|
"""Delete a survey"""
|
||||||
|
if survey_id not in surveys_db:
|
||||||
|
return jsonify({"error": "Survey not found"}), 404
|
||||||
|
|
||||||
|
del surveys_db[survey_id]
|
||||||
|
return jsonify({"success": True, "message": "Survey deleted"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys', methods=['DELETE'])
|
||||||
|
def delete_all_surveys():
|
||||||
|
"""Delete all surveys"""
|
||||||
|
count = len(surveys_db)
|
||||||
|
surveys_db.clear()
|
||||||
|
return jsonify({"success": True, "message": f"Deleted {count} surveys"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/<survey_id>/responses', methods=['POST'])
|
||||||
|
def submit_response(survey_id):
|
||||||
|
"""Submit survey responses"""
|
||||||
|
if survey_id not in surveys_db:
|
||||||
|
return jsonify({"error": "Survey not found"}), 404
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
response_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
response_entry = {
|
||||||
|
'id': response_id,
|
||||||
|
'survey_id': survey_id,
|
||||||
|
'responses': data.get('responses', {}),
|
||||||
|
'photos': data.get('photos', []),
|
||||||
|
'analyzed': False,
|
||||||
|
'analysis': None,
|
||||||
|
'submitter_name': data.get('submitter_name', 'Anonymous'),
|
||||||
|
'created_at': datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
surveys_db[survey_id]['responses'].append(response_entry)
|
||||||
|
return jsonify({"response": response_entry}), 201
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/<survey_id>/photos', methods=['POST'])
|
||||||
|
def upload_photo(survey_id):
|
||||||
|
"""Upload a photo for a survey"""
|
||||||
|
if survey_id not in surveys_db:
|
||||||
|
return jsonify({"error": "Survey not found"}), 404
|
||||||
|
|
||||||
|
if 'photo' not in request.files:
|
||||||
|
return jsonify({"error": "No photo provided"}), 400
|
||||||
|
|
||||||
|
photo = request.files['photo']
|
||||||
|
if photo.filename == '':
|
||||||
|
return jsonify({"error": "No photo selected"}), 400
|
||||||
|
|
||||||
|
# Save photo
|
||||||
|
filename = secure_filename(f"{survey_id}_{uuid.uuid4()}_{photo.filename}")
|
||||||
|
photo_path = os.path.join(PHOTOS_FOLDER, filename)
|
||||||
|
photo.save(photo_path)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"filename": filename,
|
||||||
|
"url": f"/api/photos/{survey_id}/{filename}"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/photos/<survey_id>/<filename>', methods=['GET'])
|
||||||
|
def get_photo(survey_id, filename):
|
||||||
|
"""Get a photo"""
|
||||||
|
photo_path = os.path.join(PHOTOS_FOLDER, filename)
|
||||||
|
if os.path.exists(photo_path):
|
||||||
|
return send_file(photo_path)
|
||||||
|
return jsonify({"error": "Photo not found"}), 404
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/<survey_id>/analyze', methods=['POST'])
|
||||||
|
def analyze_survey(survey_id):
|
||||||
|
"""Analyze survey with AI"""
|
||||||
|
if survey_id not in surveys_db:
|
||||||
|
return jsonify({"error": "Survey not found"}), 404
|
||||||
|
|
||||||
|
survey = surveys_db[survey_id]
|
||||||
|
data = request.get_json() or {}
|
||||||
|
selected_model = data.get('model', 'llava:latest')
|
||||||
|
|
||||||
|
# Get the latest response
|
||||||
|
if not survey['responses']:
|
||||||
|
return jsonify({"error": "No responses to analyze"}), 400
|
||||||
|
|
||||||
|
last_response = survey['responses'][-1]
|
||||||
|
responses = last_response['responses']
|
||||||
|
|
||||||
|
# Build analysis prompt
|
||||||
|
prompt = f"""Analyze this IT site survey and provide recommendations:
|
||||||
|
|
||||||
|
Survey: {survey['name']}
|
||||||
|
Client: {survey['client_name']}
|
||||||
|
Site: {survey['site_name']}
|
||||||
|
|
||||||
|
Responses:
|
||||||
|
"""
|
||||||
|
|
||||||
|
for q in survey['questions']:
|
||||||
|
qid = q['id']
|
||||||
|
if qid in responses:
|
||||||
|
prompt += f"\n{q['question']}: {responses[qid]}"
|
||||||
|
|
||||||
|
prompt += "\n\nProvide a detailed analysis including:\n"
|
||||||
|
prompt += "1. Network infrastructure recommendations\n"
|
||||||
|
prompt += "2. Equipment recommendations with estimated costs\n"
|
||||||
|
prompt += "3. Implementation timeline\n"
|
||||||
|
prompt += "4. Potential challenges and solutions\n"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Call Ollama for analysis
|
||||||
|
ollama_data = {
|
||||||
|
"model": selected_model,
|
||||||
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
|
"stream": False
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = requests.post(OLLAMA_URL, json=ollama_data, timeout=300)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
result = resp.json()
|
||||||
|
analysis = result.get('message', {}).get('content', 'No analysis available')
|
||||||
|
|
||||||
|
# Save analysis
|
||||||
|
last_response['analyzed'] = True
|
||||||
|
last_response['analysis'] = analysis
|
||||||
|
last_response['model_used'] = selected_model
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"analysis": analysis,
|
||||||
|
"model_used": selected_model
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({"error": f"Ollama error: {resp.status_code}"}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/surveys/<survey_id>/export', methods=['POST'])
|
||||||
|
def export_survey(survey_id):
|
||||||
|
"""Export survey results"""
|
||||||
|
if survey_id not in surveys_db:
|
||||||
|
return jsonify({"error": "Survey not found"}), 404
|
||||||
|
|
||||||
|
data = request.get_json() or {}
|
||||||
|
export_format = data.get('format', 'json')
|
||||||
|
|
||||||
|
survey = surveys_db[survey_id]
|
||||||
|
|
||||||
|
if export_format == 'json':
|
||||||
|
return jsonify({"survey": survey})
|
||||||
|
elif export_format == 'text':
|
||||||
|
# Generate text report
|
||||||
|
text = f"IT Site Survey Report\n"
|
||||||
|
text += f"Survey: {survey['name']}\n"
|
||||||
|
text += f"Client: {survey['client_name']}\n"
|
||||||
|
text += f"Site: {survey['site_name']}\n"
|
||||||
|
text += f"Date: {survey['created_at']}\n\n"
|
||||||
|
|
||||||
|
for response in survey['responses']:
|
||||||
|
text += f"Response by: {response['submitter_name']}\n"
|
||||||
|
for q in survey['questions']:
|
||||||
|
qid = q['id']
|
||||||
|
if qid in response['responses']:
|
||||||
|
text += f"{q['question']}: {response['responses'][qid]}\n"
|
||||||
|
if response.get('analysis'):
|
||||||
|
text += f"\nAI Analysis:\n{response['analysis']}\n"
|
||||||
|
|
||||||
|
return jsonify({"text": text})
|
||||||
|
else:
|
||||||
|
return jsonify({"error": "Unsupported format"}), 400
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=3003, debug=False)
|
||||||
+1601
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user