frimeet-api-nlp / README.md
AlleksDev's picture
Clarifitions precision
6384165 unverified
|
Raw
History Blame Contribute Delete
21.7 kB
metadata
title: Frimeet API NLP
sdk: docker
app_port: 7860
pinned: false

Frimeet API NLP

Servicio NLP independiente para busqueda semantica con FastText + pgvector, recomendaciones y redaccion conversacional con Llama via Groq.

La API principal sigue siendo la fuente de verdad de lugares, posts, usuarios, sesiones, permisos y reportes. Este servicio NLP solo trabaja con datos derivados para busqueda semantica.

Arquitectura

API principal
  |-- fuente de verdad de places/posts
  `-- consume la API NLP por REST

Hugging Face API NLP
  |-- usa credenciales nlp_reader
  |-- consulta RDS PostgreSQL + pgvector
  |-- genera el embedding FastText del query del usuario
  |-- ordena por similitud coseno en pgvector
  `-- usa Groq/Llama para embellecer recomendaciones y chat

Hugging Face Jobs
  |-- usan credenciales nlp_writer
  |-- consumen endpoints paginados/cursor de la API principal
  |-- generan embeddings por batch
  `-- hacen upsert via funciones SQL controladas

RDS PostgreSQL + pgvector
  |-- place_embeddings
  |-- post_embeddings
  |-- match_places
  |-- match_posts
  |-- get_place_content_hashes
  |-- get_post_content_hashes
  |-- upsert_place_embedding
  `-- upsert_post_embedding

La API crea el cliente RDS con rol reader. Los jobs crean el cliente con rol writer. Si configuras PGVECTOR_READER_* y PGVECTOR_WRITER_* en el mismo entorno, el codigo elige automaticamente las credenciales correctas para cada flujo.

En Hugging Face, guarda passwords y tokens como Secrets.

Cache Local Efimera

La API usa cache local en memoria para:

  • embeddings de queries repetidas,
  • resultados de busqueda/recomendacion frecuentes.

Este cache vive solo mientras el contenedor de Hugging Face este despierto. Si el Space se duerme o reinicia, se pierde sin problema porque RDS sigue siendo la fuente persistente de embeddings derivados.

Instalacion Local

python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt
Copy-Item .env.example .env

Ejecutar API

uvicorn app.main:app --host 127.0.0.1 --port 8080 --reload

Documentacion local:

http://127.0.0.1:8080/docs

Hugging Face Docker Space

El proyecto incluye Dockerfile para Hugging Face Spaces. El contenedor expone el puerto 7860 y arranca:

uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860}

Durante el build, Docker descarga model.bin desde el repositorio oficial facebook/fasttext-es-vectors y lo guarda en /opt/models/fasttext-es/model.bin. La capa queda cacheada, por lo que un cambio normal de codigo no vuelve a descargar el modelo de varios GB.

Endpoints

GET  /health
GET  /ready
POST /places/search
GET  /places/search/metrics?k=5
POST /places/search/metrics?k=5
POST /places/recommendations
POST /places/chat
POST /internal/places/chat
POST /posts/recommendations
POST /internal/posts/feed/rank
POST /internal/posts/clusters/runs
GET  /internal/posts/clusters/status
GET  /internal/posts/clusters/runs/{run_id}
POST /internal/posts/clusters/runs/{run_id}/activate
POST /search

Sync Jobs

Probar sin escribir:

python -m app.jobs.sync_place_embeddings --mode snapshot --dry-run --max-pages 1
python -m app.jobs.sync_post_embeddings --mode snapshot --max-pages 1
python -m app.jobs.sync_search_embeddings --resource all --dry-run --max-pages 1

Primera carga:

python -m app.jobs.initial_load_place_embeddings
python -m app.jobs.initial_load_post_embeddings
python -m app.jobs.initial_load_search_embeddings --resource all

Sincronizaciones posteriores:

python -m app.jobs.sync_place_embeddings --mode incremental
python -m app.jobs.sync_post_embeddings --mode incremental
python -m app.jobs.sync_feed_interactions
python -m app.jobs.rebuild_user_interest_profiles
python -m app.jobs.train_post_clusters
python -m app.jobs.sync_search_embeddings --resource all

Los jobs calculan un content_hash versionado con el contenido, modelo, version y dimension. Un cambio de modelo fuerza la regeneracion aunque el texto no haya cambiado. El job de lugares consume GET /api/v1/internal/places/snapshot para la carga inicial y GET /api/v1/internal/places/changes para sincronizaciones posteriores. Tambien consulta GET /api/v1/places/categories?lang=es una vez por ejecucion y sincroniza etiquetas localizadas, atributos, entretenimiento, elementos contenidos y menu. Si el catalogo espanol no esta disponible o llega vacio, el job falla sin reindexar con etiquetas en ingles; se debe desplegar primero la API principal y volver a intentarlo.

La sincronizacion de users, clubs, groups y events consume exclusivamente los snapshots internos de la API principal:

/api/v1/internal/search/users/snapshot
/api/v1/internal/search/clubs/snapshot
/api/v1/internal/search/groups/snapshot
/api/v1/internal/search/events/snapshot

Estos clientes requieren MAIN_API_INTERNAL_TOKEN; nunca usan un JWT de usuario ni hacen fallback a MAIN_API_AUTH_TOKEN.

Antes del primer job semantico ejecuta, en orden, sql/migrations/20260716_02_places_semantic_v1.sql y sql/migrations/20260721_03_place_facets_and_incremental_sync.sql en la base pgvector.

Busqueda Global Hibrida

La app movil debe llamar POST /api/v1/search en la API principal con su token de sesion. Go valida permisos y filtros de negocio, llama al endpoint interno POST /internal/search/candidates de NLP, hidrata los IDs y devuelve los recursos completos. NLP solo calcula candidatos y puntajes; no es la fuente de verdad ni devuelve datos de presentacion.

El query genera un solo embedding FastText. Cada proveedor combina similitud coseno con full-text search de PostgreSQL mediante Reciprocal Rank Fusion. Los resultados que no superan el umbral semantico ni el lexical se descartan: no se agregan recursos de relleno. Los eventos se descartan antes del ranking cuando start_time + duration_minutes <= as_of.

POST /search permanece como endpoint publico de compatibilidad temporal. Puede deshabilitarse con PUBLIC_GLOBAL_SEARCH_ENABLED=false cuando la app movil ya use solo la API principal.

{
  "query": "club universitario de ajedrez",
  "resource_types": ["clubs", "groups", "events", "users"],
  "per_type_limit": 5,
  "top_limit": 10,
  "requester_id": "b83ab97e-91a4-4f69-b102-b27c6092e9cb"
}

La API principal obtiene la identidad desde el token de login; la app no debe enviar requester_id como autoridad. El endpoint interno de NLP exige Authorization: Bearer <NLP_SERVICE_TOKEN> y devuelve exclusivamente IDs y puntajes.

El indice de usuarios excluye correo, fecha de nacimiento, genero y ubicacion actual. Los IDs de tags de eventos se conservan como metadatos; para aportar significado semantico la API principal debe enviar tambien sus nombres.

Chat interno de recomendaciones de lugares

La app movil no debe consumir la API NLP directamente. Debe enviar mensaje, conversacion y ubicacion actual a la API principal; Go llama POST /internal/places/chat, hidrata los IDs devueltos, calcula distancias con PostGIS y aplica el orden geografico final.

NLP separa categoria, preferencias, exclusiones, referencia y alcance geografico antes de buscar. is_active=true, ciudad/estado confirmados y los IDs obtenidos por un filtro geografico explicito son restricciones duras; la categoria es una hipotesis de ranking. Esto permite recuperar vocabulario nuevo o categorias distintas entre sistemas sin confundir una referencia como "cerca del parque" con el tipo de resultado solicitado. Las ambiguedades que cambiarian los resultados devuelven action=clarification y un state_patch con pending_clarification; el siguiente turno puede resolverlo con frases como "la primera opcion" o "la segunda, cerca de mi".

Los turnos puramente sociales (hola, ola, gracias, despedidas y confirmaciones) se responden sin ejecutar clasificacion, filtro geografico ni retrieval. Un saludo que tambien contiene una busqueda, por ejemplo hola, recomiendame una cafeteria, conserva la intencion de lugares. Las hipotesis de una aclaracion solo se muestran cuando superan el umbral semantico y cuentan con candidatos locales suficientes; sus IDs siguen siendo tecnicos, pero sus etiquetas fallback son legibles y la localizacion final pertenece a la API principal.

El chat no exige que el usuario nombre siempre una categoria. Puede habilitar un BERT fine-tuneado de token classification para extraer valores abiertos de categoria, preferencia, exclusion, ubicacion, referencia y radio. Esos textos se alinean despues contra un catalogo dinamico mediante embeddings; no se convierten con aliases dentro del adaptador BERT. Las reglas lexicas existentes quedan como fallback de despliegue y no bloquean retrieval. El clasificador se abstiene si la similitud es baja o dos conceptos quedan demasiado cerca. Las aclaraciones usan hipotesis con evidencia o facetas de los candidatos recuperados, no un menu fijo.

La recuperacion combina dense retrieval (FastText de rollback o SentenceTransformer), BM25 y coincidencias de facetas. La categoria y las exclusiones aportan señales positivas o negativas; no eliminan candidatos por una coincidencia textual aislada. NLP devuelve candidatos tecnicos y content_score; el GPS se aplica como filtro explicito de IDs antes del ranking cuando el proveedor de lugares cercanos esta configurado. El flag inicial es PLACES_CHAT_V2_ENABLED=false y debe activarse despues de desplegar en Go tanto el proxy de chat como /api/v1/internal/places/resolve-anchor.

Un radio implicito inicia con PLACES_CHAT_DEFAULT_RADIUS_METERS y, si no produce lugares o evidencia de la categoria, puede ampliarse hasta PLACES_CHAT_MAX_AUTO_RADIUS_METERS. Un radio escrito por el usuario nunca se amplia. El radio efectivo queda en location_directive.radius_meters; los place_ids usados para cada consulta son transitorios y no se persisten en state_patch.

La integracion complementaria ya esta implementada en la API principal Go. Sus contratos se documentan en docs/cambios_api_principal_chat_lugares.md y docs/cambios_app_movil_chat_lugares.md.

SQL RDS

Contrato de referencia:

sql/aws_pgvector_contract.sql

Esquemas exactos:

docs/pgvector_place_embeddings_schema.md
docs/pgvector_post_embeddings_schema.md

Ese SQL debe ejecutarse una vez con un rol administrador/DBA fuera de Hugging Face. La API NLP usa solo nlp_reader; los jobs usan solo nlp_writer.

El feed requiere, en este orden para una BD ya existente:

sql/migrate_post_feed_v1.sql
sql/verify_post_feed_v1.sql
sql/migrate_post_feed_v2.sql
sql/verify_post_feed_v2.sql

sql/rollback_post_feed_v1.sql es el rollback destructivo de emergencia. No se debe ejecutar ninguna de estas migraciones desde la API ni desde un job.

La busqueda coordinada por la API principal requiere, para una BD existente:

sql/migrations/20260712_01_global_search_candidate_filters.sql
sql/verify_global_search_candidate_filters.sql

La migracion reemplaza de forma transaccional search_resource_embeddings y agrega dos helpers de parseo seguro para timestamps y duraciones. No crea ni modifica tablas, columnas o indices. El segundo archivo es de solo lectura, falla con una excepcion si detecta deriva y debe ejecutarse despues para comprobar el contrato.

/ready exige la firma y los marcadores V2 de search; una funcion heredada ya no puede declarar el servicio listo. Las conexiones y consultas pgvector usan REQUEST_TIMEOUT_SECONDS, y las rutas tienen un limite local configurable con RATE_LIMIT_REQUESTS_PER_WINDOW, INTERNAL_RATE_LIMIT_REQUESTS_PER_WINDOW y RATE_LIMIT_WINDOW_SECONDS.

Para una BD vacía o una BD existente que ya use VECTOR(300), puede ejecutarse sql/new_pgvector_schema.sql desde pgAdmin. El archivo es convergente e incluye el contrato base, feed V1, correcciones V2, propietarios y permisos. No convierte VECTOR(16); esa conversión sigue usando la migración FastText separada.

Los perfiles no se actualizan dentro del request móvil. Configura un scheduler (cron, EventBridge o worker) con estos comandos, en orden:

python -m app.jobs.sync_post_embeddings --mode incremental
python -m app.jobs.sync_feed_interactions

Una frecuencia inicial razonable es cada minuto. El entrenamiento de clusters se ejecuta aparte con python -m app.jobs.train_post_clusters; con pocos posts ajusta KMEANS_MIN_POSTS, KMEANS_MIN_K y KMEANS_MIN_CLUSTER_SIZE sin usar k >= n.

El endpoint POST /internal/posts/clusters/runs agenda el entrenamiento en background y responde 202. Para operacion normal se recomienda el job python -m app.jobs.train_post_clusters. Si KMEANS_AUTO_ACTIVATE=false, activa el run validado con POST /internal/posts/clusters/runs/{run_id}/activate.

Migracion De VECTOR(16) A FastText VECTOR(300)

La guia operativa completa esta en docs/fasttext_deployment.md.

Los vectores son datos derivados. Para esta migracion no se intenta convertir los 16 valores mock en 300 valores semanticos: se vacian ambas tablas y se reconstruyen desde la API principal.

Con la API NLP y los jobs pausados, ejecuta como nlp_owner o administrador:

psql "host=<host> port=5432 dbname=nlp_vectors user=<admin> sslmode=require" -f sql/migrate_fasttext_300.sql
psql "host=<host> port=5432 dbname=nlp_vectors user=<admin> sslmode=require" -f sql/aws_pgvector_contract.sql

Despues configura las variables FastText, despliega la nueva imagen y repuebla:

python -m app.jobs.initial_load_place_embeddings
python -m app.jobs.initial_load_post_embeddings
psql "host=<host> port=5432 dbname=nlp_vectors user=<admin> sslmode=require" -f sql/verify_fasttext_embeddings.sql

La verificacion debe reportar dimension 300, modelo facebook/fasttext-es-vectors y normas cercanas a 1.

Ranking Semantico FastText Y Llama Via Groq

/places/search y /places/recommendations aplican el flujo de Lab5_Embeddings_Busqueda_Semantica.ipynb: tokenizan el texto, obtienen los vectores FastText de cada termino, calculan su promedio, normalizan el documento y consultan pgvector mediante similitud coseno. FastText usa subpalabras, por lo que puede relacionar variantes morfologicas y palabras fuera de vocabulario.

Las requests y responses HTTP no cambian. Las metricas existentes ahora describen el motor fasttext_mean_embeddings, similitud coseno y dimension 300. match_quality usa SEMANTIC_NO_MATCH_THRESHOLD y SEMANTIC_RELEVANCE_THRESHOLD.

Ambos endpoints aceptan filtro geografico mediante lat, lng y radius en metros. Cuando se proporcionan coordenadas, el servicio NLP consulta GET /api/v1/places/nearby en la API principal y limita pgvector a los IDs devueltos. Las coordenadas siguen perteneciendo a la API principal; no es necesario guardarlas en pgvector, truncar tablas ni regenerar embeddings.

{
  "query": "un lugar tranquilo para cenar cerca de mi",
  "lat": 16.7531,
  "lng": -93.1156,
  "radius": 10000,
  "limit": 5
}

El bloque metrics tambien indica location_filter_applied, nearby_place_count y radius_meters para hacer visible la aplicacion del radio.

POST /places/recommendations no mezcla el benchmark fijo con la consulta del usuario. Si el score maximo no supera SEMANTIC_NO_MATCH_THRESHOLD, envia a Llama el modo no_match y devuelve places: []. Entre ese valor y SEMANTIC_RELEVANCE_THRESHOLD usa low_confidence; por encima usa confident. Llama solo embellece el tono y recibe exclusivamente los lugares seleccionados.

Documento Semantico Estructurado De Lugares

Los IDs numericos de tags devueltos por la API principal se resuelven mediante el catalogo versionado en app/modules/places/infrastructure/place_tag_catalog.json. El documento de Places contiene cada señal una sola vez y explicita el rol de cada campo. Esto evita que la repeticion manual distorsione un encoder BERT:

Nombre: ... Categoria: <etiqueta en español> <valor canonico> ...
Atributos confirmados: ... Entretenimiento disponible: ...
Elementos y actividades: ... Menu: ...

No se expanden categorias mediante diccionarios de sinonimos. Direccion, ciudad, estado, source, precio e IDs desconocidos permanecen fuera del embedding; siguen disponibles como metadatos o filtros. Los atributos NULL se consideran desconocidos y no restan relevancia; false/no se conservan como ausencia explicita, pero no se insertan como evidencia positiva. La version structured-place-v4 forma parte del hash y fuerza un re-embedding seguro cuando cambia el documento.

Migracion BERT/Sentence-Transformer exclusiva de Places

La guia operativa completa para Colab, backfill, verificacion, cutover y rollback esta en docs/places_semantic_deployment.md.

La migracion es aditiva y no cambia los vectores de posts, perfiles o feed:

  1. Ejecuta sql/migrations/20260716_02_places_semantic_v1.sql con el rol DBA.
  2. Configura temporalmente el perfil BERT mostrado en .env.example.
  3. Ejecuta python -m app.jobs.sync_place_embeddings para backfill de la tabla place_embeddings_semantic_v1.
  4. Ejecuta sql/verify_places_semantic_v1.sql y revisa que el plan use HNSW con un volumen representativo.
  5. Activa match_places_semantic_v1 y search_places_semantic_v1 primero en shadow.
  6. Conserva match_places y la tabla FastText para rollback.

/places/chat ya no requiere una categoria canonica para recuperar candidatos. La categoria inferida solo aporta afinidad al ranking; la union SQL obtiene pools dense y lexical independientes. Un cliente puede optar al contrato conversacional estructurado enviando conversation_id, conversation_state, clarification_choice o user_location mientras PLACES_CHAT_V2_ENABLED=true.

Para fine-tuning, scripts/train_place_retriever.py acepta JSONL con query, positive y hard_negatives. El artefacto resultante se configura mediante PLACES_EMBEDDING_MODEL; no se incluye un modelo ficticio preentrenado en el repo.

La evaluación del retriever debe usar el corpus global, no cuatro candidatos aislados por fila. Validation incluye 60 consultas y el test sintético 80 consultas, cada uno sobre 20 documentos propios y no vistos, con lenguaje coloquial, errores ortográficos, necesidades implícitas y frases contrastivas. Para comparar el modelo base y el fine-tuned sobre exactamente el mismo corpus:

python scripts/evaluate_place_retriever.py `
  --test-file data/training/places_retrieval_v1/test.jsonl `
  --model base=intfloat/multilingual-e5-base `
  --model fine_tuned=C:\ruta\al\places-e5-retriever-v1 `
  --output-json artifacts/retriever-evaluation.json

Además de Top-1, Recall@k, MRR y nDCG@10, el reporte separa resultados para colloquial, misspelling, implicit, contrastive y otras dificultades. El prefijo query:/passage: se aplica dentro del evaluador.

El extractor de intencion se entrena por separado con scripts/train_place_intent_bert.py. Su JSONL contiene text y spans abiertos {start, end, slot}; los slots permitidos son CATEGORY, PREFERENCE, EXCLUSION, LOCATION, REFERENCE y RADIUS. Los valores concretos (por ejemplo "donas artesanales") nunca se convierten en labels del modelo:

python -m pip install -r requirements-training.txt
python scripts/train_place_intent_bert.py `
  --train-file data/places-intent-train.jsonl `
  --validation-file data/places-intent-validation.jsonl `
  --output-dir .models/places-intent-bert

Para activarlo, configura PLACES_CHAT_INTENT_PROVIDER=bert, PLACES_CHAT_BERT_MODEL_PATH=.models/places-intent-bert y una version inmutable en PLACES_CHAT_BERT_MODEL_VERSION. El parser determinista queda como fallback si el modelo no puede cargarse; cuando BERT responde, no se vuelven a aplicar aliases de categoria ni implicaciones manuales sobre sus spans.

Una imagen que ya no necesite el artefacto de rollback FastText puede construirse con docker build --build-arg DOWNLOAD_FASTTEXT_MODEL=false .. Conserva el valor por defecto durante el shadow/canary para permitir rollback inmediato.

GET o POST /places/search/metrics?k=5 conserva un benchmark offline separado llamado built_in_places_v3_bm25. Contiene doce lugares controlados, diez consultas y qrels graduados para calcular honestamente Precision@k, Recall@k, MRR, MAP y nDCG@k. Estas metricas requieren juicios de relevancia y por eso no se presentan como si midieran una consulta arbitraria de produccion.

La respuesta incluye metric_definitions con etiquetas y descripciones claras, y recommended_metric con nDCG@k como metrica principal sugerida para la app movil. nDCG@k es apropiada para recomendaciones de lugares porque considera el orden y permite relevancia graduada.

GET /places/search/metrics?k=5

Para actualizar funciones o permisos sin cambiar nuevamente la dimension, vuelve a ejecutar sql/aws_pgvector_contract.sql. No repitas la migracion destructiva una vez que las columnas ya sean VECTOR(300).

Groq/Llama se usa en /places/recommendations, /places/chat y, opcionalmente, en /internal/places/chat para redactar una respuesta conversacional. No decide que lugares recomendar, no hace busqueda y no inventa lugares. Desactivar PLACES_CHAT_LLM_ENABLED no cambia la accion ni los candidatos del chat interno.

El arreglo estructurado places viene desde RDS/pgvector mediante embeddings, filtros y ranking. La app debe renderizar cards desde ese arreglo, no parseando texto libre del LLM.

Tests

pytest