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