import { STATE, CONFIG } from '../core/State.js'; /** Build-time VITE_API_URL; empty → same-origin (monolito HF en :7860). */ function resolveApiUrl() { const fromEnv = import.meta.env.VITE_API_URL; if (fromEnv && String(fromEnv).trim() !== '') { return String(fromEnv).trim(); } if (typeof window !== 'undefined' && window.location?.origin) { return window.location.origin; } return 'http://localhost:8000'; } const API_URL = resolveApiUrl(); export class RemoteProvider { constructor() { this.ready = false; this.modelName = 'Remote Engine v2'; } async init() { try { if (CONFIG.debug) console.log(`[RemoteProvider] Connecting to ${API_URL}...`); const res = await fetch(`${API_URL}/health`); if (!res.ok) throw new Error('Backend not healthy'); const data = await res.json(); if (CONFIG.debug) console.log(`[RemoteProvider] Connected! Model: ${data.model}`); // Capture Metadata STATE.modelName = data.model || 'Unknown Model'; STATE.appMode = data.app_mode || 'local'; STATE.isSandboxed = !!data.is_sandboxed; // Smart Labeling if (API_URL.includes('localhost') || API_URL.includes('127.0.0.1')) { STATE.providerType = 'LOCAL (FastAPI)'; } else { STATE.providerType = 'REMOTE (FastAPI)'; } this.ready = true; // Load model registry (Fase 4: multi-modelo). Define el modelo activo y la // selección inicial de la UI a partir del backend. try { const modelsRes = await fetch(`${API_URL}/models`); if (modelsRes.ok) { const md = await modelsRes.json(); STATE.models = md.models || []; STATE.activeModelKey = md.active_model_key || STATE.activeModelKey; STATE.defaultModelKey = md.default_model_key || STATE.defaultModelKey; STATE.selectedModelKey = STATE.activeModelKey; } } catch (err) { console.error("[RemoteProvider] Failed to load model registry:", err); } // Load dimension dictionary from backend try { const dictRes = await fetch(`${API_URL}/dimension_dictionary`); if (dictRes.ok) { const dictData = await dictRes.json(); STATE.dimensionDictionary = dictData; for (const [dim, entry] of Object.entries(dictData)) { if (entry && entry.name) { STATE.dimensionLabels[dim] = entry.name.toUpperCase(); } } } } catch (err) { console.error("[RemoteProvider] Failed to load dimension dictionary:", err); } } catch (e) { console.error("[RemoteProvider] Connection Failed:", e); // Fallback Metadata STATE.modelName = 'N/A'; STATE.providerType = 'OFFLINE / LOCAL'; alert("⚠️ Cannot connect to Python Backend. Is 'server.py' running?"); } } // Pide el vector al servidor (POST /embed) async getEmbedding(text) { if (!this.ready) return null; try { const res = await fetch(`${API_URL}/embed`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: text }) }); const data = await res.json(); // Convertimos la lista de Python a Float32Array para que Three.js la entienda return { embedding: new Float32Array(data.embedding), token_id: data.token_id || 0 }; } catch (e) { console.error("Embedding Error:", e); return null; } } // Pide la interpretación de la dimensión (POST /analyze_dimension) async getDimensionAnalysis(dimensionIndex, space = STATE.featureSpace || 'RAW') { if (!this.ready) return null; const cacheKey = `${space}_${dimensionIndex}`; // Check Cache if (STATE.dimensionCache && STATE.dimensionCache[cacheKey]) { if (CONFIG.debug) console.log(`[RemoteProvider] Cache Hit for Dim ${cacheKey}`); return STATE.dimensionCache[cacheKey]; } try { const res = await fetch(`${API_URL}/analyze_dimension`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dimension_index: dimensionIndex, top_k: 5, space: space }) }); const data = await res.json(); // Store in Cache if (STATE.dimensionCache) { STATE.dimensionCache[cacheKey] = data; } return data; } catch (e) { console.error("Analysis Error:", e); return null; } } // Pide la aritmética vectorial (POST /arithmetic) async getArithmetic(wordA, wordB, wordC, topK = 5, space = STATE.featureSpace || 'RAW') { if (!this.ready) return null; try { const res = await fetch(`${API_URL}/arithmetic`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ word_a: wordA, word_b: wordB, word_c: wordC, top_k: topK, space: space }) }); const data = await res.json(); // Convertir vector a Float32Array if (data.vector) { data.vector = new Float32Array(data.vector); } return data; } catch (e) { console.error("Arithmetic Error:", e); return null; } } // Pide la tokenización y embeddings (POST /tokenize) async tokenizeAndEmbed(text) { if (!this.ready) return { tokens: [] }; try { const res = await fetch(`${API_URL}/tokenize`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: text }) }); if (!res.ok) throw new Error('Tokenization failed'); return await res.json(); // Returns { tokens: [...] } } catch (e) { console.error("Tokenization Error:", e); return { tokens: [] }; } } // Ingesta un archivo PDF con all-mpnet-base-v2 (768D, espacio unificado) async ingestPDF(file, chunkSize = 512, chunkOverlap = 50, normalizeWhitespace = true, removeSpecialChars = false) { if (!this.ready) { throw new Error("El motor remoto no está listo. Verifica la conexión con el servidor."); } const formData = new FormData(); formData.append('file', file); formData.append('chunk_size', chunkSize.toString()); formData.append('chunk_overlap', chunkOverlap.toString()); formData.append('normalize_whitespace', normalizeWhitespace ? 'true' : 'false'); formData.append('remove_special_chars', removeSpecialChars ? 'true' : 'false'); try { const res = await fetch(`${API_URL}/ingest_pdf`, { method: 'POST', body: formData }); if (!res.ok) { throw await this._handleResponseError(res); } return await res.json(); // { status: "ok", filename: "...", chunks: N, message: "..." } } catch (e) { console.error("[RemoteProvider] Ingest PDF Error:", e); throw e; } } // Retorna la lista de archivos PDF indexados async listReliefFiles() { if (!this.ready) return { files: [], count: 0 }; try { const res = await fetch(`${API_URL}/relief_files`); if (!res.ok) { throw await this._handleResponseError(res); } return await res.json(); // { files: [...], count: N } } catch (e) { console.error("[RemoteProvider] List Relief Files Error:", e); return { files: [], count: 0 }; } } // Obtiene la matriz del PDF para visualizar. // `modelKey` (Fase 4) lee el cache de un modelo específico sin cargarlo en RAM, // permitiendo el mismo documento bajo varios modelos lado a lado. Ignorado en SAE // (el SAE vive sobre el espacio del modelo por defecto, 768D). async getReliefMatrix(filename, space = 'RAW', modelKey = null) { if (!this.ready) { throw new Error("El motor remoto no está listo. Verifica la conexión con el servidor."); } try { const endpoint = space === 'SAE' ? 'sae/get_relief_matrix' : 'get_relief_matrix'; let url = `${API_URL}/${endpoint}?filename=${encodeURIComponent(filename)}`; if (space !== 'SAE' && modelKey) url += `&model=${encodeURIComponent(modelKey)}`; const res = await fetch(url); if (!res.ok) { throw await this._handleResponseError(res); } return await res.json(); // { filename, chunks_count, dimensions_count, matrix, text_metadata, model_key } } catch (e) { console.error("[RemoteProvider] Get Relief Matrix Error:", e); throw e; } } // Registro de modelos de embeddings soportados y cuál está activo (GET /models) async getModels() { if (!this.ready) return { models: [], active_model_key: null, active_dim: null, default_model_key: null }; try { const res = await fetch(`${API_URL}/models`); if (!res.ok) throw await this._handleResponseError(res); return await res.json(); } catch (e) { console.error("[RemoteProvider] Get Models Error:", e); return { models: [], active_model_key: null, active_dim: null, default_model_key: null }; } } // Lanza la re-ingesta de TODOS los PDFs indexados bajo un modelo (POST /reingest). // Devuelve { job_id, model, total, files }. El progreso se consulta con reingestStatus. async reingest(modelKey) { if (!this.ready) { throw new Error("El motor remoto no está listo. Verifica la conexión con el servidor."); } const res = await fetch(`${API_URL}/reingest?model=${encodeURIComponent(modelKey)}`, { method: 'POST' }); if (!res.ok) throw await this._handleResponseError(res); return await res.json(); } // Estado de un job de re-ingesta (GET /reingest/status) async reingestStatus(jobId) { if (!this.ready) return null; const res = await fetch(`${API_URL}/reingest/status?job_id=${encodeURIComponent(jobId)}`); if (!res.ok) throw await this._handleResponseError(res); return await res.json(); } // Solicita cancelar un job de re-ingesta (POST /reingest/cancel) async reingestCancel(jobId) { if (!this.ready) return null; const res = await fetch(`${API_URL}/reingest/cancel?job_id=${encodeURIComponent(jobId)}`, { method: 'POST' }); if (!res.ok) throw await this._handleResponseError(res); return await res.json(); } // Realiza una búsqueda semántica interactiva en el relieve async searchRelief(query, filename, topK = 5) { if (!this.ready) { throw new Error("El motor remoto no está listo. Verifica la conexión con el servidor."); } try { const url = `${API_URL}/search_relief?query=${encodeURIComponent(query)}&filename=${encodeURIComponent(filename)}&top_k=${topK}`; const res = await fetch(url); if (!res.ok) { throw await this._handleResponseError(res); } return await res.json(); // { similarities: [...], top_indices: [...] } } catch (e) { console.error("[RemoteProvider] Search Relief Error:", e); throw e; } } // Obtiene el diccionario de dimensiones persistido en el servidor async getDimensionDictionary() { if (!this.ready) return {}; try { const res = await fetch(`${API_URL}/dimension_dictionary`); if (!res.ok) throw await this._handleResponseError(res); return await res.json(); } catch (e) { console.error("[RemoteProvider] Get Dimension Dictionary Error:", e); return {}; } } // Guarda/actualiza una entrada en el diccionario de dimensiones del servidor async updateDimensionDictionary(dim, entry) { if (!this.ready) return false; try { const res = await fetch(`${API_URL}/dimension_dictionary/${dim}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entry) }); if (!res.ok) throw await this._handleResponseError(res); return true; } catch (e) { console.error("[RemoteProvider] Update Dimension Dictionary Error:", e); return false; } } // Obtiene el estado del SAE desde el backend async getSAEStatus() { if (!this.ready) return { is_trained: false, config: null, metrics: null }; try { const res = await fetch(`${API_URL}/sae/status`); if (!res.ok) throw await this._handleResponseError(res); return await res.json(); } catch (e) { console.error("[RemoteProvider] Get SAE Status Error:", e); return { is_trained: false, config: null, metrics: null }; } } // Solicita iniciar el entrenamiento del SAE con hiperparámetros async trainSAE(hiddenDim = 8192, k = 32, epochs = 50, lr = 0.001, batchSize = 64) { if (!this.ready) { throw new Error("El motor remoto no está listo. Verifica la conexión con el servidor."); } try { const res = await fetch(`${API_URL}/sae/train`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hidden_dim: hiddenDim, k: k, epochs: epochs, lr: lr, batch_size: batchSize }) }); if (!res.ok) throw await this._handleResponseError(res); return await res.json(); } catch (e) { console.error("[RemoteProvider] Train SAE Error:", e); throw e; } } // Solicita la propuesta de nombre asistida por IA para una dimensión async nameDimension(dimensionIndex, filenames = [], space = 'RAW') { if (!this.ready) { throw new Error("El motor remoto no está listo. Verifica la conexión con el servidor."); } try { const res = await fetch(`${API_URL}/name_dimension`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dimension_index: dimensionIndex, top_k: 8, filenames: filenames, space: space }) }); if (!res.ok) throw await this._handleResponseError(res); return await res.json(); } catch (e) { console.error("[RemoteProvider] Name Dimension Error:", e); throw e; } } // Solicita los top K activadores y inhibidores para una dimensión async getTopChunksForDimension(dimensionIndex, filenames = [], space = 'RAW', topK = 8) { if (!this.ready) { throw new Error("El motor remoto no está listo. Verifica la conexión con el servidor."); } try { const res = await fetch(`${API_URL}/top_chunks_for_dimension`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dimension_index: dimensionIndex, top_k: topK, filenames: filenames, space: space }) }); if (!res.ok) throw await this._handleResponseError(res); return await res.json(); } catch (e) { console.error("[RemoteProvider] Get Top Chunks Error:", e); throw e; } } // Helper privado para parsear errores de respuesta (JSON o Texto) de FastAPI async _handleResponseError(res) { let errMsg = 'Failed request'; try { const contentType = res.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { const errJson = await res.json(); if (errJson && errJson.detail) { errMsg = errJson.detail; } else if (errJson && errJson.message) { errMsg = errJson.message; } else { errMsg = JSON.stringify(errJson); } } else { const errText = await res.text(); if (errText) errMsg = errText; } } catch (_) { // fallback } return new Error(errMsg); } }