AlleksDev commited on
Commit
dfc1129
·
unverified ·
1 Parent(s): 8e1a6ed

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
- target_category = explicit_category or state.target_category
 
 
 
 
 
 
126
  category_changed = bool(
127
- explicit_category
128
  and state.target_category
129
- and explicit_category != state.target_category
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=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=explicit_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
- confidence = 0.96 if explicit_category else 0.84
 
 
 
 
 
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",