Saltar al contenido principal

Pipeline de enriquecimiento

Última actualización: 14 de abril de 2026 Audiencia: cualquiera depurando "¿qué hace exactamente Clione con mi producto?"


TL;DR

Clione transforma un producto en bruto (título, descripción, precio, vendor, imágenes) en una representación semántica multi-formato que:

  1. Permite que los LLMs (ChatGPT, Perplexity, Gemini) entiendan el producto con precisión
  2. Permite que los motores de búsqueda crawleen datos estructurados ricos (schema.org)
  3. Permite que la búsqueda vectorial recupere productos por intención (similitud semántica)
  4. Permite que los storefronts inyecten widgets de FAQ y señales JSON-LD automáticamente

Produce 6 categorías de artefactos a partir de una única llamada al LLM más una llamada al embedding:

#ArtefactoDónde viveQuién lo consume
1Texto semántico (core_identity, reasoning)Columna DB en product_embeddingsLLMs, búsqueda interna, dashboard
2Quality scores (durability, quality_perception, value_for_money, price_positioning)Columna DB synthetic_propertiesDashboard, scoring, ranking
3Search keywords (array bilingüe)Columna DB search_keywordsBúsqueda full-text, tag clouds
4Vector embedding (array float de 1536 dims)Columna pgvectorBúsqueda por similitud coseno
5JSON-LD (schema.org/Product)Generado on-the-fly en /products/:id.jsonldMotores de búsqueda, crawlers de LLMs
6Señales SEO meta (descripción, keywords, canonical)Generado on-the-fly en /products/:id.metaHerramientas SEO, storefronts headless

Más a nivel de tenant: /.well-known/llms.txt (manifiesto del catálogo) y /.well-known/ai-offers.json (contexto de promociones).


Pipeline (de extremo a extremo)

┌─────────────────┐
│ Plataforma (Shopify, BigCommerce, etc.)
│ Envía producto en bruto:
│ título, descripción, precio, vendor, imágenes, categorías
└────────┬────────┘


┌─────────────────────────────────────────────────────────┐
│ PASO 1 · Adapter normaliza la forma específica de la │
│ plataforma │
│ packages/platform-shopify/src/adapters/product.ts │
│ → RawProduct │
└────────┬────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ PASO 2 · Enriquecimiento por LLM (Normalizer) │
│ packages/semantic-core/src/services/normalizer.ts │
│ Llama a llmClient.enrich() con el prompt en: │
│ apps/products-api/src/lib/promptTemplates.ts │
│ │
│ Modelo en producción: gpt-4o-mini │
│ Fallback en dev: Ollama (llama3.2:1b para el LLM, │
│ nomic-embed-text para el embedding) │
│ │
│ Devuelve: │
│ core_identity: "descripción semántica de 1-2 frases" │
│ reasoning: { recommended_for, decision_logic, │
│ objection_handler, seasonal_relevance } │
│ synthetic_properties: { durability_score, │
│ quality_perception, value_for_money, │
│ price_positioning, typical_competitors, │
│ search_keywords (bilingüe EN+ES) } │
└────────┬────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ PASO 3 · Ensamblaje del texto del embedding │
│ packages/storage/src/embeddings/embedding-text.ts │
│ Concatena con separador " | ": │
│ core_identity + recommended_for + decision_logic │
│ + title + vendor + search_keywords │
└────────┬────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ PASO 4 · Generación del vector embedding │
│ Producción: OpenAI text-embedding-3-small (1536 dims) │
│ Dev: Ollama nomic-embed-text (768 dims) │
│ Resultado: float[1536] L2-normalizado (listo para coseno)│
└────────┬────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ PASO 5 · Persistencia + snapshot de versión │
│ packages/storage/src/vector/pgvector.ts │
│ Hace upsert en la tabla product_embeddings (una fila) │
│ Hace snapshot de la versión anterior en │
│ product_enrichment_versions (se conservan las 3 últimas)│
└─────────────────────────────────────────────────────────┘

Esquema de almacenamiento

product_embeddings

ColumnaTipoContenido
idTEXTID de producto en la plataforma
tenant_idTEXTaislamiento multi-tenant
tenant_platform_idTEXTaislamiento por tienda
platformTEXTshopify / bigcommerce / etc.
core_identityTEXTdescripción semántica generada por LLM
reasoningJSONB{ recommended_for, decision_logic, objection_handler, seasonal_relevance }
synthetic_propertiesJSONB{ durability_score, quality_perception, value_for_money, price_positioning, typical_competitors, search_keywords }
technical_detailsJSONB{ original_title, vendor, price, currency, images }
metadataJSONB{ transformed_at, llm_model, transformation_version, manually_edited?, last_manual_edit_at?, enrichment_pending? }
search_keywordsTEXT[]array desnormalizado para búsqueda FT
embeddingvector(1536)el vector real, normalizado para coseno

product_enrichment_versions (versionado, últimas 3 conservadas por producto)

Snapshot de core_identity + reasoning + synthetic_properties + search_keywords + metadata más version, reason ("manual re-enrich" / "bulk re-enrich pending" / "rollback to vN" / "initial sync"), created_by, created_at. Lo usa la pestaña History + el endpoint de rollback.


Endpoints que exponen los artefactos

Por producto

EndpointContent-TypePropósito
GET /api/{platform}/products/:idapplication/jsonPayload SemanticProduct completo (todo lo de arriba)
GET /api/{platform}/products/:id.llmapplication/json (X-Content-Format: llm-optimized)JSON optimizado para LLM: identidad, reasoning, scores, market context, texto computado summary_for_ai
GET /api/{platform}/products/:id.jsonldapplication/ld+jsonschema.org/Product con additionalProperty[] cargando los scores como PropertyValue
GET /api/{platform}/products/:id.metaapplication/jsonSeñales SEO: meta_description (160/300/full), URL canonical, keywords[]

Nivel tenant / dominio

EndpointFormatoPropósito
GET /.well-known/llms.txttext/plain (Markdown según spec llms.txt)Manifiesto del catálogo: nombre de la tienda, número de productos, resumen de enriquecimiento, endpoints de API, guía de uso
GET /.well-known/ai-offers.jsonapplication/jsonContexto de promociones (gestionado por promotions-engine)

Embed de storefront

embed.js es un loader de un solo fichero servido en /embed.js que:

  • Auto-detecta tipo/ID de entidad a partir de patrones de URL + meta tags OpenGraph
  • Recupera datos de FAQ vía /api/v1/public/faq/render
  • Inyecta (a) acordeón HTML visible (b) schema JSON-LD FAQPage invisible

Validado por origin contra la lista blanca allowed_domains de la API key.


Quality Scores — limitaciones importantes

Lee esto antes de mostrarle scores a un cliente.

Los 5 campos de calidad (durability_score, quality_perception, value_for_money, price_positioning, typical_competitors) son hoy 100% inferidos por el LLM.

Qué usa el LLM

  • Título
  • Descripción
  • Precio + moneda
  • Vendor
  • Categorías
  • enrichment_hints del Wizard si están rellenas

Qué NO usa el LLM

  • Reseñas reales (Google / Trustpilot / internas)
  • Tasa de devoluciones
  • Datos de ventas / tasa de conversión
  • Comparativas externas de catálogos
  • Certificaciones de fabricante (ISO, MIL-STD, GOTS, etc.)
  • Resultados de tests de ciclo de vida
  • Bases de datos de sostenibilidad

Implicaciones

  1. Los scores son opinión sintética, no hechos verificados.
  2. El mismo producto puede dar scores distintos entre runs si la temperatura del modelo no es cero.
  3. Útiles relativamente: el LLM tiene sentido común (Hermès → lujo, Primark → budget), así que los scores ayudan a diferenciar dentro del mismo catálogo, no contra benchmarks externos.
  4. typical_competitors tiene el mismo problema — son inferencias plausibles, no datos de mercado verificados.

Grounding — capas shippeadas

  • Badge de disclaimer visual en cada bloque de Quality Scores, resuelto desde metadata.scores_evidence_level: 🟢 Data-grounded · 🔵 AI + owner hints · 🟡 AI-inferred
  • Enrichment Wizard tiene una sección Evidence dedicada con 5 preguntas (warranty_period, certifications, returns_rate, reviews_rating, manufacturing_origin). Rellenar 3+ sube el badge a verde; 1-2 a cyan. La lógica vive en lib/evidence-grounding.ts y aplica tanto a los endpoints de enrich de Shopify como de BigCommerce.
  • Typical Competitors proporcionados por el owner estructurados como [{ brand, model, ref_price?, notes? }] vía el Edit tab del producto. Cuando hay al menos una fila guardada, metadata.competitors_source = 'owner' y el LLM deja de regenerar la lista en re-enrichment (lib/preserve-owner-data.ts).

Roadmap pendiente

Conectores de datos externos — webhooks de Returns rate, APIs de reviews Judge.me + Yotpo, GA conversion rate — y el schema de procedencia per-score (synthetic_properties.scores_provenance) con resolución compuesta. Diseño completo en docs/SCORES_GROUNDING_ROADMAP.md (interno).

Hasta que lleguen los conectores externos, asume que los scores reflejan la lectura del LLM del texto del catálogo afinada por la evidencia + competidores que pasaste vía el wizard — no es ground truth, pero es mejor que síntesis pura.


Aviso sobre cambio de modelo de embedding

La columna embedding está fijada como vector(1536) para coincidir con text-embedding-3-small. Cambiar a un modelo de embedding distinto con otra dimensión (p. ej. nomic-embed-text de Ollama, 768 dims) requiere re-indexar todo — habría que cambiar el tipo de columna y regenerar cada embedding existente. No es un toggle en runtime.


Cuándo re-enriquecer

EscenarioAcción
Sincronización inicial desde la plataformaAuto-enriquecido como parte del import
El título o la descripción del catálogo cambia en origenEl re-sync dispara re-enriquecimiento
El owner edita campos de IA manualmente en el dashboardSe guarda con metadata.manually_edited = true. Los re-enriquecimientos futuros muestran una confirmación extra "OVERWRITE" para evitar perder el trabajo manual
El owner lanza el Enrichment Wizard con contexto nuevoRe-enriquecimiento manual con las hints embebidas en el prompt
Upgrade del modelo LLM (p. ej. gpt-4o-mini → siguiente versión)Re-enriquecimiento masivo recomendado; las versiones viejas se conservan durante 3 generaciones vía pestaña History

El re-enriquecimiento es destructivo pero versionado: la salida anterior se snapshotea en product_enrichment_versions y se puede restaurar desde la pestaña History. Retención: las últimas 3 versiones por producto.


Por qué importa

Todo el diseño asume que el agent commerce es el próximo canal de descubrimiento. ChatGPT compra en Shopify, Perplexity recomienda, Gemini compara. Las tiendas cuyos catálogos se leen claramente para los LLMs ganan; las que no, son invisibles.

El trabajo de Clione es hacer que cada producto sea legible para esos agentes en cada formato que prefieran — sin que el merchant tenga que pensar en ello.

Por eso generamos 6 artefactos, no solo un vector.