llm-semantic-visualizer / src /data /RemoteProvider.js
hbauzan's picture
Release v4.17.7 — sync from GitHub main
819a47f
Raw
History Blame Contribute Delete
17.7 kB
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);
}
}