require 'webrick' require 'fileutils' require 'net/http' require 'uri' require 'json' PORT = 8001 DOC_ROOT = '.' # Current directory (must be root NOTEBOOKLM) # Load environment variables from .env or env.txt file securely without gems # (Permite usar env.txt para que no se oculte en el explorador de archivos del NAS) # Buscamos en varios sitios por si el mapeo de Docker lo deja en /app o en /app/src ['.env', 'env.txt', 'src/.env', 'src/env.txt', '../.env', '../env.txt'].each do |env_file| if File.exist?(env_file) File.readlines(env_file).each do |line| next if line.strip.start_with?('#') || line.strip.empty? key, value = line.strip.split('=', 2) ENV[key] = value.to_s.gsub(/(^"|"$|^'|'$)/, '') if key && value end end end # Set API Key securely via env OPENAI_API_KEY = ENV['OPENAI_API_KEY'] || '' class AIApiServlet < WEBrick::HTTPServlet::AbstractServlet def do_POST(req, res) res['Content-Type'] = 'application/json' res['Access-Control-Allow-Origin'] = '*' begin body = JSON.parse(req.body) base64_image = body['image'] # Expected: data:image/jpeg;base64,/9j/4AA... client_key = body['client_api_key'] # Usamos la clave del cliente si la envía, sino la del servidor active_api_key = (client_key && !client_key.empty?) ? client_key : OPENAI_API_KEY if active_api_key.nil? || active_api_key.empty? res.status = 500 res.body = { error: "API Key requerida (ni el servidor ni el cliente la proporcionaron)." }.to_json return end if base64_image.nil? || base64_image.empty? res.status = 400 res.body = { error: "No se proporcionó imagen válida." }.to_json return end # Connect to OpenAI uri = URI('https://api.openai.com/v1/chat/completions') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri.path) request['Content-Type'] = 'application/json' request['Authorization'] = "Bearer #{active_api_key}" prompt = <<~PROMPT Actúa como un ingeniero geotécnico experto interpretando gráficas de ensayos PET (Pile Echo Tester) / Integridad de pilotes de baja deformación. He subido una imagen de una gráfica de reflectograma sónico. Analiza la gráfica visualmente y extrae estos datos exactos en formato JSON puro. Reglas MUY ESTRICTAS de inferencia: 1. projectName: Extrae el texto OCR de arriba que suele poner "Project: [NOMBRE]". Devuelve el nombre. Si no lo ves, devuelve "Desconocido". 2. pileName: Extrae el texto OCR que suele poner "Pile name: [NOMBRE]" o similar. Si no lo ves, devuelve "Desconocido". 3. length: La longitud total del pilote estimada en metros (ejemplo: 12.5). Búscala como el último gran pico de reflexión clara al final de la gráfica, normalmente cruzado por una línea vertical roja de final de pilote. 4. defects: En lugar de un solo defecto, fíjate en TODO el perfil desde el 0 hasta la punta. Los pilotes reales NO son perfectos. Busca todas las fluctuaciones, crestas, valles o resaltos significativos en la curva azul de impedancia. - Por cada variación significativa o defecto, crea un objeto en este array. - 'depth': La profundidad exacta en metros de esa variación (ej: 1.7, 4.5, 10.8). - 'severity': Si el pico va hacia arriba/derecha es "estriccion_severa" o "estriccion_leve". Si va hacia abajo/izquierda es "ensanchamiento_severo" o "ensanchamiento_leve". 5. status: "danger" si hay alguna estriccion severa, "warning" si hay desviaciones leves, "ok" si no hay defectos. EJEMPLO DE RESPUESTA ESPERADA (Devuelve SOLO esto, asegurándote de encontrar TODAS las curvas a lo largo del fuste para modelar un perfil continuo 3D realista): { "projectName": "LIDL-TORREDELMAR", "pileName": "P58-A", "length": 11.0, "defects": [ { "depth": 1.7, "severity": "estriccion_leve" }, { "depth": 4.5, "severity": "ensanchamiento_leve" }, { "depth": 10.8, "severity": "ensanchamiento_severo" } ], "status": "warning" } PROMPT payload = { model: "gpt-4o", temperature: 0.0, seed: 12345, messages: [ { role: "user", content: [ { type: "text", text: prompt }, { type: "image_url", image_url: { url: base64_image } } ] } ], max_tokens: 300 } request.body = payload.to_json response = http.request(request) ai_response = JSON.parse(response.body) if ai_response['error'] res.status = 500 res.body = { error: "Error de OpenAI: #{ai_response['error']['message']}" }.to_json else ai_message = ai_response.dig('choices', 0, 'message', 'content') || "{}" # Limpiar backticks si el modelo los añade clean_json = ai_message.gsub("```json\n", "").gsub("\n```", "").strip res.status = 200 res.body = clean_json end rescue => e res.status = 500 res.body = { error: "Error interno del servidor: #{e.message}" }.to_json end end def do_OPTIONS(req, res) res['Access-Control-Allow-Origin'] = '*' res['Access-Control-Allow-Methods'] = 'POST, OPTIONS' res['Access-Control-Allow-Headers'] = 'Content-Type' res.status = 204 end end class ExpertDiagnosisServlet < WEBrick::HTTPServlet::AbstractServlet def do_POST(req, res) res['Content-Type'] = 'application/json' res['Access-Control-Allow-Origin'] = '*' begin body = JSON.parse(req.body) analysis_data = body['analysisData'] # El JSON con projectName, pileName, length, defects, status client_key = body['client_api_key'] active_api_key = (client_key && !client_key.empty?) ? client_key : OPENAI_API_KEY if active_api_key.nil? || active_api_key.empty? res.status = 500 res.body = { error: "API Key requerida para el diagnóstico experto." }.to_json return end if analysis_data.nil? || analysis_data['defects'].nil? res.status = 400 res.body = { error: "Datos de análisis estructural incompletos." }.to_json return end # Connect to OpenAI uri = URI('https://api.openai.com/v1/chat/completions') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri.path) request['Content-Type'] = 'application/json' request['Authorization'] = "Bearer #{active_api_key}" prompt = <<~PROMPT Actúa como un Ingeniero Geotécnico Senior experto en la norma ASTM D5882 (Standard Test Method for Low Strain Impact Integrity Testing of Deep Foundations). Acabamos de realizar un ensayo sónico PET/PIT a un pilote y el software ha extraído las siguientes características estructurales: Datos del pilote: #{analysis_data.to_json} Tu tarea es redactar un "Diagnóstico Experto" breve y profesional (máximo 1 o 2 párrafos concisos) dirigido al director de obra. Reglas de redacción: 1. Formato: Devuelve la respuesta en formato de texto plano estructurado o Markdown ligero (usa negritas para enfatizar), pero NO devuelvas JSON. 2. Tono: Formal, técnico y conciso. 3. Contenido: Documenta las posibles causas geológicas o constructivas de los defectos (estricciones o ensanchamientos) encontrados en esas cotas específicas (ej: desconches, cambios de estrato, bulbos por arenas sueltas, roturas del fuste durante extracción de camisa). 4. Si el status es 'ok', confirma la integridad estructural del pilote indicando que la onda ha bajado limpiamente hasta la punta. 5. Cita expresamente la "Norma ASTM D5882" para dar rigor al diagnóstico. PROMPT payload = { model: "gpt-4o", temperature: 0.3, # Ligera creatividad pero enfocado messages: [ { role: "user", content: prompt } ], max_tokens: 500 } request.body = payload.to_json response = http.request(request) ai_response = JSON.parse(response.body) if ai_response['error'] res.status = 500 res.body = { error: "Error de OpenAI en diagnóstico: #{ai_response['error']['message']}" }.to_json else expert_diagnosis = ai_response.dig('choices', 0, 'message', 'content') || "Sin diagnóstico disponible." res.status = 200 res.body = { diagnosis: expert_diagnosis }.to_json end rescue => e res.status = 500 res.body = { error: "Error interno del servidor experto: #{e.message}" }.to_json end end def do_OPTIONS(req, res) res['Access-Control-Allow-Origin'] = '*' res['Access-Control-Allow-Methods'] = 'POST, OPTIONS' res['Access-Control-Allow-Headers'] = 'Content-Type' res.status = 204 end end # Standard WEBrick Server server = WEBrick::HTTPServer.new( Port: PORT, DocumentRoot: DOC_ROOT, BindAddress: '0.0.0.0', AccessLog: [], Logger: WEBrick::Log.new($stdout, WEBrick::Log::INFO) ) server.mount '/api/analyze-pet', AIApiServlet server.mount '/api/expert-diagnosis', ExpertDiagnosisServlet trap('INT') { server.shutdown } trap('TERM') { server.shutdown } puts "[SERVER] Keller Dashboard (with AI Bridge) — port #{PORT}" server.start