99from ovos_bus_client .session import SessionManager , Session
1010from ovos_config .config import Configuration
1111from ovos_plugin_manager .templates .pipeline import ConfidenceMatcherPipeline , IntentHandlerMatch
12- from ovos_spec_tools import closest_lang , standardize_lang , SpecMessage , gate_satisfied
12+ from ovos_spec_tools import closest_lang , standardize_lang , SpecMessage , gate_satisfied , \
13+ expand , MalformedTemplate
1314from ovos_utils import flatten_list
1415from ovos_utils .fakebus import FakeBus
1516from ovos_utils .log import LOG , log_deprecation
@@ -241,13 +242,37 @@ def handle_detach_skill(self, message):
241242 if en ["name" ].startswith (skill_id_colon ):
242243 self .__detach_entity (en ["name" ], en ["lang" ])
243244
244- def _register_object (self , message , object_name , register_func ):
245+ def _valid_samples (self , samples , topic , name , lang ):
246+ """Drop malformed template samples, keeping the valid ones.
247+
248+ OVOS-INTENT-4 §6.3/§5.3 — each sample that fails template expansion
249+ is skipped with a WARN naming the owning skill, the intent/entity,
250+ the lang, the topic and the reason; the remaining samples are still
251+ indexed. An empty return means the registration must be rejected.
252+ """
253+ skill_id = name .split (':' )[0 ] if ':' in name else None
254+ valid = []
255+ for sample in samples :
256+ try :
257+ expand (sample )
258+ valid .append (sample )
259+ except MalformedTemplate as e :
260+ LOG .warning (f"skipping malformed sample on { topic } : "
261+ f"skill_id={ skill_id !r} name={ name !r} "
262+ f"lang={ lang !r} reason={ e } " )
263+ return valid
264+
265+ def _register_object (self , message , object_name , register_func , lang ):
245266 """Generic method for registering a padacioso object.
246267
247268 Args:
248269 message (Message): trigger for action
249270 object_name (str): type of entry to register
250271 register_func (callable): function to call for registration
272+ lang (str): standardized language of the registration
273+
274+ Returns:
275+ bool: True if something was registered
251276 """
252277 file_name = message .data .get ('file_name' )
253278 samples = message .data .get ("samples" )
@@ -257,15 +282,23 @@ def _register_object(self, message, object_name, register_func):
257282
258283 if (not file_name or not isfile (file_name )) and not samples :
259284 LOG .error ('Could not find file ' + file_name )
260- return
285+ return False
261286
262287 if not samples and isfile (file_name ):
263288 with open (file_name ) as f :
264289 samples = [line .strip () for line in f .readlines ()]
265290
291+ samples = self ._valid_samples (samples , message .msg_type , name , lang )
292+ if not samples : # §6.3 — reject only when nothing valid remains
293+ LOG .warning (f"rejecting { object_name } registration on "
294+ f"{ message .msg_type } : name={ name !r} lang={ lang !r} "
295+ f"reason=no valid samples remain" )
296+ return False
297+
266298 register_func (name , samples )
267299 # the container was mutated; drop stale cached matches
268300 _calc_padacioso_intent .cache_clear ()
301+ return True
269302
270303 def register_intent (self , message ):
271304 """Messagebus handler for registering intents.
@@ -276,16 +309,18 @@ def register_intent(self, message):
276309 lang = message .data .get ('lang' , self .lang )
277310 lang = standardize_lang (lang )
278311 if lang in self .containers :
279- self .registered_intents .append (message .data ['name' ])
280- self ._store_context_gate (message .data ['name' ], message .data )
281312 try :
282- self ._register_object (message , 'intent' ,
283- self .containers [lang ].add_intent )
313+ registered = self ._register_object (
314+ message , 'intent' , self .containers [lang ].add_intent , lang )
284315 except RuntimeError :
285316 name = message .data .get ('name' , "" )
286317 # padacioso fails on reloading a skill, just ignore
287318 if name not in self .containers [lang ].intent_samples :
288319 raise
320+ registered = True
321+ if registered :
322+ self .registered_intents .append (message .data ['name' ])
323+ self ._store_context_gate (message .data ['name' ], message .data )
289324
290325 def register_entity (self , message ):
291326 """Messagebus handler for registering entities.
@@ -296,9 +331,9 @@ def register_entity(self, message):
296331 lang = message .data .get ('lang' , self .lang )
297332 lang = standardize_lang (lang )
298333 if lang in self .containers :
299- self .registered_entities . append (message . data )
300- self ._register_object ( message , 'entity' ,
301- self .containers [ lang ]. add_entity )
334+ if self ._register_object (message , 'entity' ,
335+ self .containers [ lang ]. add_entity , lang ):
336+ self .registered_entities . append ( message . data )
302337
303338 # ------------------------------------------------------------------
304339 # OVOS-INTENT-4 bus handlers (consumed alongside the legacy topics)
@@ -339,6 +374,10 @@ def handle_register_template(self, message: Message):
339374 return
340375
341376 name = self ._internal_name (skill_id , intent_name )
377+ samples = self ._valid_samples (samples , topic , name , lang )
378+ if not samples : # §6.3 — reject only when nothing valid remains
379+ self ._warn_malformed (topic , data , "no valid samples remain" )
380+ return
342381 # §8.1 replacement is implicit: a re-registration replaces the prior entry
343382 self .__detach_intent (name )
344383 self .registered_intents .append (name )
@@ -374,6 +413,10 @@ def handle_register_entity(self, message: Message):
374413 return
375414
376415 name = self ._internal_name (skill_id , entity_name )
416+ samples = self ._valid_samples (samples , topic , name , lang )
417+ if not samples : # §7.2 — reject only when nothing valid remains
418+ self ._warn_malformed (topic , data , "no valid samples remain" )
419+ return
377420 # §8.1 replacement is implicit
378421 self .__detach_entity (name , lang )
379422 self .registered_entities = [
0 commit comments