> ## Documentation Index
> Fetch the complete documentation index at: https://docs.camellapp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Búsqueda conversacional

> Cuándo la búsqueda pregunta, de dónde sale el dónde, cómo se ordena por barrio y cómo funciona la búsqueda por foto.

La búsqueda del inicio interpreta lo que escribe la persona y decide si ya puede mostrar resultados o si debe preguntar algo. Pregunta solo lo que falta y nunca encierra al usuario: "Ver resultados igual" siempre está visible.

> [!NOTE]
>
> Esta página documenta el mecanismo tal como está implementado en `src/lib/search-interpret.ts`, `src/lib/search-interpret-shared.ts`, `src/lib/search-location-shared.ts`, `src/lib/search-conversation-shared.ts` y `src/lib/search-openai.ts`. Los endpoints son `POST /api/search/interpret` y `POST /api/search/interpret-image`; sus contratos están en `docs/openapi-auth.yaml`.

## Regla: qué + dónde

Una consulta es explícita cuando tiene **qué** (servicio o problema) y **dónde** (zona). Quién, cuándo y cómo no se preguntan.

* **Qué** está resuelto cuando la interpretación es de confianza alta (reglas locales u OpenAI; basta una categoría), cuando el usuario eligió un servicio en la tarjeta o cuando lo clasificó una foto. Un `keyword_fallback` (confianza baja) no cuenta: se pregunta.
* **Dónde** está resuelto si viene de cualquier fuente (ver más abajo).

La respuesta trae `missing`: `null` si ambos están resueltos, o `"que"` / `"donde"`. Si faltan los dos, primero se pregunta el qué. `question` trae el texto de la pregunta:

| `missing` | `question` |
| - | - |
| `que` | ¿Qué necesitas arreglar o hacer? |
| `donde` | ¿En qué barrio o ciudad lo necesitas? |

## Máximo 2 preguntas

`MAX_SEARCH_QUESTIONS` es 2 y nunca se pregunta dos veces lo mismo. El servidor no guarda la conversación: el cliente reenvía en cada turno el objeto `turn` con lo acumulado.

| Campo de `turn` | Qué lleva |
| - | - |
| `asked` | Las W ya preguntadas (`que`, `donde`), sin repetir. |
| `resolved` | El qué ya resuelto: `serviceIds` y `categoryIds` (chip o foto). |
| `queAnswer` | Respuesta escrita al qué; se concatena a la consulta original. |
| `dondeAnswer` | Ubicación dada o editada en la tarjeta. |

Cuando se agotan las 2 preguntas, `missing` vuelve `null` y se va a resultados con lo que haya. Los consumidores que no envían `turn` reciben lo mismo que antes y pueden ignorar los campos nuevos.

## Cuándo se llama a OpenAI

* Si las reglas locales no resuelven el qué (como antes).
* Si las reglas sí lo resolvieron pero no hay dónde en la respuesta ni en el campo del buscador, y el perfil no tiene ciudad: se llama para extraer el dónde del texto.
* Si el dónde ya viene del campo, no hay llamada extra.
* Si el perfil tiene ciudad, tampoco se llama. La tarjeta se abre con esa ciudad y ese barrio.
* Una respuesta escrita al dónde, sin ciudad, se separa en ciudad y barrio con OpenAI cuando hay `OPENAI_API_KEY`.

El timeout es de 8 segundos. Si OpenAI falla o no hay `OPENAI_API_KEY`, se degrada a reglas y fallback; nunca hay un 500 por la IA. Sin la clave, el dónde sale de la respuesta o del campo. Si solo está el perfil, la tarjeta pregunta con la ciudad y el barrio ya escritos.

## De dónde sale el dónde

Se elige **una sola fuente**, sin mezclar ciudad de una con barrio de otra. Prioridad:

1. `answer`: lo que el usuario respondió o editó en la tarjeta.
2. `query`: el lugar que OpenAI extrajo del texto de la consulta, ya separado en ciudad y barrio.
3. `field`: el campo "Ubicación" del buscador (texto libre).
4. `profile`: la ciudad y el barrio guardados en el perfil. No cierran la búsqueda: rellenan la tarjeta para confirmarlos o cambiarlos. Se leen en el servidor con la sesión, no viajan desde el cliente.

El resultado trae `location` con `source`, `city`, `neighborhood` y `text`.

## Ciudad filtra, barrio ordena

`GET /api/camellos` recibe la ubicación según lo que se sepa:

* **Con ciudad conocida** (`city`): filtra por ciudad. Si además llega `barrio`, los camellos de ese barrio salen primero dentro del orden elegido y el resto va después (incluido quien no tiene barrio). El barrio nunca excluye. El orden por barrio aplica con paginación por `page`; la respuesta incluye `barrioTotal`.
* **Texto libre sin ciudad** (`ubicacion`): comportamiento clásico, la ciudad o el barrio contienen el texto.

Una búsqueda con qué resuelto nunca termina vacía por la ubicación:

* Si en el barrio no hay nadie (`barrioTotal` es 0), se muestran los de la ciudad con este aviso, que nombra el servicio, el barrio y la ciudad: "No encontramos plomeros en Chapinero, pero encontramos plomeros en Bogotá."
* Si con la ubicación la primera página queda vacía, el servidor repite la consulta sin ella y marca `locationWidened: "pais"`. El aviso es: "No encontramos plomeros en Chapinero; te mostramos los de todo el país." Sin barrio ni ciudad dice "en esa zona".

Si no se conoce el servicio, los avisos usan "profesionales". Los textos salen de `locationNoticeText`. `/explorar` muestra la ubicación aplicada como una píldora que se puede quitar.

## Búsqueda por imagen

`POST /api/search/interpret-image` recibe un `multipart/form-data` con `image` y, opcional, `ubicacion`. Una foto del problema se clasifica contra el catálogo con un modelo de visión y entra al mismo flujo: con el qué resuelto, si hay dónde se va a resultados y si no se pregunta el dónde. Si la IA no logra clasificarla, la tarjeta pregunta el qué.

* **Límites:** los de las demás imágenes subidas, JPEG, PNG o WebP de hasta 2 MB (`src/lib/upload-image-shared.ts`). Los errores son "Solo se permiten JPEG, PNG o WebP" y "El archivo supera el tamano maximo (2 MB)".
* **Privacidad:** la foto se procesa en memoria; no se escribe en disco ni se guarda.
* **Modelo:** `OPENAI_VISION_MODEL`; si falta, `OPENAI_MODEL`, y si tampoco está, `gpt-6-luna`.
* **Disponibilidad:** el botón de cámara solo se muestra con `OPENAI_API_KEY` configurada. Es público, también sin sesión.
* **Rate limit:** cupo propio por IP de 5 solicitudes por minuto, más bajo que el del texto porque una foto cuesta más.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.