Spaces:
Running
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:
- Ejecuta
sql/migrations/20260716_02_places_semantic_v1.sqlcon el rol DBA. - Configura temporalmente el perfil BERT mostrado en
.env.example. - Ejecuta
python -m app.jobs.sync_place_embeddingspara backfill de la tablaplace_embeddings_semantic_v1. - Ejecuta
sql/verify_places_semantic_v1.sqly revisa que el plan use HNSW con un volumen representativo. - Activa
match_places_semantic_v1ysearch_places_semantic_v1primero en shadow. - Conserva
match_placesy 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