Spaces:
Sleeping
Sleeping
| 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); | |
| } | |
| } | |