--- 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 ```text 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 ```powershell python -m venv .venv .\.venv\Scripts\Activate.ps1 python -m pip install -r requirements.txt Copy-Item .env.example .env ``` ## Ejecutar API ```powershell uvicorn app.main:app --host 127.0.0.1 --port 8080 --reload ``` Documentacion local: ```text 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: ```bash 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 ```http 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: ```powershell 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: ```powershell 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: ```powershell 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: ```text /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. ```json { "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 ` 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: ```text sql/aws_pgvector_contract.sql ``` Esquemas exactos: ```text 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: ```text 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: ```text 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: ```powershell 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: ```powershell psql "host= port=5432 dbname=nlp_vectors user= sslmode=require" -f sql/migrate_fasttext_300.sql psql "host= port=5432 dbname=nlp_vectors user= sslmode=require" -f sql/aws_pgvector_contract.sql ``` Despues configura las variables FastText, despliega la nueva imagen y repuebla: ```powershell python -m app.jobs.initial_load_place_embeddings python -m app.jobs.initial_load_post_embeddings psql "host= port=5432 dbname=nlp_vectors user= 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. ```json { "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: ```text Nombre: ... Categoria: ... 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: ```powershell 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: ```powershell 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. ```http 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 ```powershell pytest ```