Spaces:
Sleeping
Sleeping
Fix: Excesive aclarations
Browse files
README.md
CHANGED
|
@@ -203,6 +203,13 @@ Las ambiguedades que cambiarian los resultados devuelven `action=clarification`
|
|
| 203 |
`state_patch` con `pending_clarification`; el siguiente turno puede resolverlo con frases
|
| 204 |
como "la primera opcion" o "la segunda, cerca de mi".
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
La recuperacion combina FastText/pgvector, BM25 y coincidencias de facetas. NLP devuelve
|
| 207 |
solo candidatos tecnicos y `content_score`; no incorpora GPS al score. El flag inicial es
|
| 208 |
`PLACES_CHAT_V2_ENABLED=false` y debe activarse despues de desplegar en Go tanto el proxy
|
|
|
|
| 203 |
`state_patch` con `pending_clarification`; el siguiente turno puede resolverlo con frases
|
| 204 |
como "la primera opcion" o "la segunda, cerca de mi".
|
| 205 |
|
| 206 |
+
El chat no exige que el usuario nombre siempre una categoria. Intenciones de actividad
|
| 207 |
+
de alta certeza usan defaults deterministas: por ejemplo, "quiero comer algo" busca
|
| 208 |
+
restaurantes, "quiero hacer ejercicio" busca opciones deportivas y "donde dormir"
|
| 209 |
+
busca hospedaje. Una categoria explicita siempre prevalece sobre estos defaults. Solo se
|
| 210 |
+
pide aclaracion cuando las interpretaciones plausibles cambiarian materialmente los
|
| 211 |
+
resultados; las preferencias faltantes no bloquean una primera recomendacion.
|
| 212 |
+
|
| 213 |
La recuperacion combina FastText/pgvector, BM25 y coincidencias de facetas. NLP devuelve
|
| 214 |
solo candidatos tecnicos y `content_score`; no incorpora GPS al score. El flag inicial es
|
| 215 |
`PLACES_CHAT_V2_ENABLED=false` y debe activarse despues de desplegar en Go tanto el proxy
|
app/modules/places/infrastructure/deterministic_intent_parser.py
CHANGED
|
@@ -38,6 +38,73 @@ _EXCLUSION_PATTERN = re.compile(
|
|
| 38 |
)
|
| 39 |
_CURRENT_LOCATION_VALUES = {"mi", "aqui", "donde estoy", "mi ubicacion"}
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
@dataclass(frozen=True)
|
| 43 |
class CategoryDefinition:
|
|
@@ -122,11 +189,17 @@ class DeterministicPlaceChatIntentParser:
|
|
| 122 |
)
|
| 123 |
|
| 124 |
explicit_category = categories[0] if categories else None
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
category_changed = bool(
|
| 127 |
-
|
| 128 |
and state.target_category
|
| 129 |
-
and
|
| 130 |
)
|
| 131 |
|
| 132 |
if location_text:
|
|
@@ -206,7 +279,7 @@ class DeterministicPlaceChatIntentParser:
|
|
| 206 |
return self._location_scope_clarification(
|
| 207 |
target_category=target_category,
|
| 208 |
category_values=category_values,
|
| 209 |
-
explicit_category=
|
| 210 |
hard_filters=hard_filters,
|
| 211 |
preferences=merged_preferences,
|
| 212 |
exclusions=merged_exclusions,
|
|
@@ -230,7 +303,7 @@ class DeterministicPlaceChatIntentParser:
|
|
| 230 |
semantic_query = " ".join(_ordered_unique(semantic_parts)).strip()
|
| 231 |
|
| 232 |
patch = ConversationStatePatch(
|
| 233 |
-
target_category=
|
| 234 |
hard_filters=hard_filters if hard_filters != state.hard_filters else None,
|
| 235 |
soft_preferences=(
|
| 236 |
merged_preferences
|
|
@@ -245,7 +318,12 @@ class DeterministicPlaceChatIntentParser:
|
|
| 245 |
clear_reference=category_changed and reference_text is None,
|
| 246 |
taxonomy_version=self._taxonomy.version,
|
| 247 |
)
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
if reference_text:
|
| 250 |
confidence -= 0.05
|
| 251 |
return ParsedPlaceChatIntent(
|
|
@@ -505,6 +583,13 @@ class DeterministicPlaceChatIntentParser:
|
|
| 505 |
)
|
| 506 |
)
|
| 507 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
def _matched_preferences(self, normalized: str) -> tuple[str, ...]:
|
| 509 |
matched: list[str] = []
|
| 510 |
for preference in self._taxonomy.preferences:
|
|
|
|
| 38 |
)
|
| 39 |
_CURRENT_LOCATION_VALUES = {"mi", "aqui", "donde estoy", "mi ubicacion"}
|
| 40 |
|
| 41 |
+
# High-precision activity intents that imply a useful place category even when
|
| 42 |
+
# the user does not name it. These defaults keep the chat action-oriented while
|
| 43 |
+
# remaining deterministic and auditable; explicit category aliases always win.
|
| 44 |
+
_ACTIVITY_CATEGORY_DEFAULTS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
| 45 |
+
(
|
| 46 |
+
"restaurant",
|
| 47 |
+
(
|
| 48 |
+
"quiero comer",
|
| 49 |
+
"quisiera comer",
|
| 50 |
+
"me gustaria comer",
|
| 51 |
+
"necesito comer",
|
| 52 |
+
"tengo hambre",
|
| 53 |
+
"algo para comer",
|
| 54 |
+
"algo de comer",
|
| 55 |
+
"comer algo",
|
| 56 |
+
"donde comer",
|
| 57 |
+
"ir a comer",
|
| 58 |
+
"salir a comer",
|
| 59 |
+
"quiero desayunar",
|
| 60 |
+
"quisiera desayunar",
|
| 61 |
+
"quiero almorzar",
|
| 62 |
+
"quisiera almorzar",
|
| 63 |
+
"quiero cenar",
|
| 64 |
+
"quisiera cenar",
|
| 65 |
+
"comida vegana",
|
| 66 |
+
"comida vegetariana",
|
| 67 |
+
"algo vegano",
|
| 68 |
+
"algo vegetariano",
|
| 69 |
+
"algo sin gluten",
|
| 70 |
+
),
|
| 71 |
+
),
|
| 72 |
+
(
|
| 73 |
+
"sports",
|
| 74 |
+
(
|
| 75 |
+
"hacer ejercicio",
|
| 76 |
+
"quiero entrenar",
|
| 77 |
+
"quisiera entrenar",
|
| 78 |
+
"donde entrenar",
|
| 79 |
+
),
|
| 80 |
+
),
|
| 81 |
+
(
|
| 82 |
+
"cinema",
|
| 83 |
+
(
|
| 84 |
+
"ver una pelicula",
|
| 85 |
+
"ver peliculas",
|
| 86 |
+
),
|
| 87 |
+
),
|
| 88 |
+
(
|
| 89 |
+
"shopping",
|
| 90 |
+
(
|
| 91 |
+
"quiero comprar algo",
|
| 92 |
+
"ir de compras",
|
| 93 |
+
"salir de compras",
|
| 94 |
+
),
|
| 95 |
+
),
|
| 96 |
+
(
|
| 97 |
+
"lodging",
|
| 98 |
+
(
|
| 99 |
+
"donde dormir",
|
| 100 |
+
"donde hospedarme",
|
| 101 |
+
"quiero hospedarme",
|
| 102 |
+
"necesito alojamiento",
|
| 103 |
+
"pasar la noche",
|
| 104 |
+
),
|
| 105 |
+
),
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
|
| 109 |
@dataclass(frozen=True)
|
| 110 |
class CategoryDefinition:
|
|
|
|
| 189 |
)
|
| 190 |
|
| 191 |
explicit_category = categories[0] if categories else None
|
| 192 |
+
inferred_category = (
|
| 193 |
+
self._inferred_activity_category(target_clause)
|
| 194 |
+
if explicit_category is None
|
| 195 |
+
else None
|
| 196 |
+
)
|
| 197 |
+
requested_category = explicit_category or inferred_category
|
| 198 |
+
target_category = requested_category or state.target_category
|
| 199 |
category_changed = bool(
|
| 200 |
+
requested_category
|
| 201 |
and state.target_category
|
| 202 |
+
and requested_category != state.target_category
|
| 203 |
)
|
| 204 |
|
| 205 |
if location_text:
|
|
|
|
| 279 |
return self._location_scope_clarification(
|
| 280 |
target_category=target_category,
|
| 281 |
category_values=category_values,
|
| 282 |
+
explicit_category=requested_category,
|
| 283 |
hard_filters=hard_filters,
|
| 284 |
preferences=merged_preferences,
|
| 285 |
exclusions=merged_exclusions,
|
|
|
|
| 303 |
semantic_query = " ".join(_ordered_unique(semantic_parts)).strip()
|
| 304 |
|
| 305 |
patch = ConversationStatePatch(
|
| 306 |
+
target_category=requested_category,
|
| 307 |
hard_filters=hard_filters if hard_filters != state.hard_filters else None,
|
| 308 |
soft_preferences=(
|
| 309 |
merged_preferences
|
|
|
|
| 318 |
clear_reference=category_changed and reference_text is None,
|
| 319 |
taxonomy_version=self._taxonomy.version,
|
| 320 |
)
|
| 321 |
+
if explicit_category:
|
| 322 |
+
confidence = 0.96
|
| 323 |
+
elif inferred_category:
|
| 324 |
+
confidence = 0.88
|
| 325 |
+
else:
|
| 326 |
+
confidence = 0.84
|
| 327 |
if reference_text:
|
| 328 |
confidence -= 0.05
|
| 329 |
return ParsedPlaceChatIntent(
|
|
|
|
| 583 |
)
|
| 584 |
)
|
| 585 |
|
| 586 |
+
@staticmethod
|
| 587 |
+
def _inferred_activity_category(normalized: str) -> str | None:
|
| 588 |
+
for category, patterns in _ACTIVITY_CATEGORY_DEFAULTS:
|
| 589 |
+
if _contains_any(normalized, patterns):
|
| 590 |
+
return category
|
| 591 |
+
return None
|
| 592 |
+
|
| 593 |
def _matched_preferences(self, normalized: str) -> tuple[str, ...]:
|
| 594 |
matched: list[str] = []
|
| 595 |
for preference in self._taxonomy.preferences:
|
docs/api_endpoints.md
CHANGED
|
@@ -410,6 +410,12 @@ silencio entre usar el parque como zona de resultados o como ayuda para identifi
|
|
| 410 |
referencia: devuelve `action=clarification`, `unresolved=["location_scope"]` y conserva
|
| 411 |
el contexto no ambiguo en `state_patch`.
|
| 412 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
## 3. Publicaciones
|
| 414 |
|
| 415 |
### `POST /posts/recommendations`
|
|
|
|
| 410 |
referencia: devuelve `action=clarification`, `unresolved=["location_scope"]` y conserva
|
| 411 |
el contexto no ambiguo en `state_patch`.
|
| 412 |
|
| 413 |
+
No es obligatorio escribir una categoria literal. El parser aplica defaults de alta
|
| 414 |
+
precision para actividades inequívocas: `quiero comer algo` se interpreta como
|
| 415 |
+
`target_category=restaurant`, mientras que una frase abierta como `quiero salir` aun
|
| 416 |
+
solicita aclaracion. Si el mensaje tambien contiene una categoria explicita, esta tiene
|
| 417 |
+
prioridad sobre el default inferido.
|
| 418 |
+
|
| 419 |
## 3. Publicaciones
|
| 420 |
|
| 421 |
### `POST /posts/recommendations`
|
tests/test_place_chat_intent_parser.py
CHANGED
|
@@ -115,6 +115,54 @@ def test_missing_category_requires_clarification() -> None:
|
|
| 115 |
assert intent.unresolved == ("target_category",)
|
| 116 |
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
def test_common_category_typo_is_normalized_by_the_taxonomy() -> None:
|
| 119 |
intent = DeterministicPlaceChatIntentParser().parse(
|
| 120 |
message="recomiendame una cafetria tranquila",
|
|
|
|
| 115 |
assert intent.unresolved == ("target_category",)
|
| 116 |
|
| 117 |
|
| 118 |
+
def test_food_activity_defaults_to_restaurant_without_clarification() -> None:
|
| 119 |
+
intent = DeterministicPlaceChatIntentParser().parse(
|
| 120 |
+
message="quiero comer algo",
|
| 121 |
+
state=ConversationState(),
|
| 122 |
+
has_user_location=True,
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
assert intent.action == "recommendations"
|
| 126 |
+
assert intent.target_category == "restaurant"
|
| 127 |
+
assert intent.category_values == ("restaurant", "restaurante")
|
| 128 |
+
assert intent.semantic_query == "restaurant"
|
| 129 |
+
assert intent.state_patch.target_category == "restaurant"
|
| 130 |
+
assert intent.confidence == 0.88
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def test_explicit_category_wins_over_an_activity_default() -> None:
|
| 134 |
+
intent = DeterministicPlaceChatIntentParser().parse(
|
| 135 |
+
message="quiero comer algo en una cafeteria",
|
| 136 |
+
state=ConversationState(),
|
| 137 |
+
has_user_location=True,
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
assert intent.action == "recommendations"
|
| 141 |
+
assert intent.target_category == "cafe"
|
| 142 |
+
assert intent.category_values == ("cafe", "cafeteria", "coffee_shop")
|
| 143 |
+
assert intent.state_patch.target_category == "cafe"
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def test_other_high_confidence_activities_use_helpful_defaults() -> None:
|
| 147 |
+
parser = DeterministicPlaceChatIntentParser()
|
| 148 |
+
examples = {
|
| 149 |
+
"quiero hacer ejercicio": "sports",
|
| 150 |
+
"quiero ver una pelicula": "cinema",
|
| 151 |
+
"quiero comprar algo": "shopping",
|
| 152 |
+
"necesito un lugar donde dormir": "lodging",
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
for message, expected_category in examples.items():
|
| 156 |
+
intent = parser.parse(
|
| 157 |
+
message=message,
|
| 158 |
+
state=ConversationState(),
|
| 159 |
+
has_user_location=True,
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
assert intent.action == "recommendations"
|
| 163 |
+
assert intent.target_category == expected_category
|
| 164 |
+
|
| 165 |
+
|
| 166 |
def test_common_category_typo_is_normalized_by_the_taxonomy() -> None:
|
| 167 |
intent = DeterministicPlaceChatIntentParser().parse(
|
| 168 |
message="recomiendame una cafetria tranquila",
|