From dbe8a2b05dc95d2dc8537967cbd06ae5a6371c67 Mon Sep 17 00:00:00 2001 From: Antonio Cordero Balcazar Date: Mon, 8 Jun 2026 16:49:15 +0200 Subject: [PATCH] * Added option to have a default choice when no other choices are available after conditions (in choices and wildcards). --- docs/SYNTAX.md | 2 ++ grammar.lark | 9 +++++---- ppp_tree.py | 17 +++++++++++++++-- ppp_wildcards.py | 2 +- tests/tests_choices.py | 7 +++++++ tests/tests_wildcards.py | 7 +++++++ tests/wildcards/test.yaml | 8 ++++++++ 7 files changed, 45 insertions(+), 7 deletions(-) diff --git a/docs/SYNTAX.md b/docs/SYNTAX.md index 0fd58ff..5f11c67 100644 --- a/docs/SYNTAX.md +++ b/docs/SYNTAX.md @@ -53,6 +53,7 @@ The choice options are as follows: * `'identifiers'`: comma separated labels for the choice (optional, quotes can be single or double). Only makes sense inside a wildcard definition. Can be used when specifying the wildcard to select this specific choice. It's case insensitive. * `n`: weight of the choice (optional, default 1). * `if condition`: filters out the choice if the condition is false (optional; this is an extension to the *Dynamic Prompts* syntax). Same conditions as in the `if` command. +* `else`: flag to indicate this is the choice to use if no other choice is available after conditions. It won't be considered if other choices are available. Also optional. * `::`: end of choice options (not optional if any options) Whitespace is allowed between parameters/options. @@ -142,6 +143,7 @@ The choices of the wildcard follow the same format as in the choices construct, If using the object format for a choice you can use the following in addition to the standard `weight` and `text`/`content`: * `if`: the condition (a string) +* `else`: flag (boolean) to indicate this is the choice to use if no other choice is available after conditions. It won't be considered if other choices are available. * `labels`: list of labels (an array of strings) * `command`: indicates the content is a command (a boolean) diff --git a/grammar.lark b/grammar.lark index 281ed60..57f30e6 100644 --- a/grammar.lark +++ b/grammar.lark @@ -165,27 +165,28 @@ extranetworktag: "<" /(?!ppp:)\w+:/ encontent ">" choicesoptions_sep: "$$" plain // choice options - choice: [ [ _WHITESPACE? choiceiscmd ] [ _WHITESPACE? choicelabels ] [ _WHITESPACE? choiceweight ] [ _WHITESPACE? choiceif ] _WHITESPACE? "::" ] choicevalue + choice: [ [ _WHITESPACE? choiceiscmd ] [ _WHITESPACE? choicelabels ] [ _WHITESPACE? choiceweight ] [ _WHITESPACE? ( choiceif | choiceelse ) ] _WHITESPACE? "::" ] choicevalue choiceiscmd: /%/ // the option text is a special command choicelabels: ( /"/ IDENTIFIER ( _WHITESPACE? "," _WHITESPACE? IDENTIFIER )* /"/ ) | ( /'/ IDENTIFIER ( _WHITESPACE? "," _WHITESPACE? IDENTIFIER )* /'/ ) choiceweight: NUMBER choiceif: "if" _WHITESPACE condition + choiceelse: "else" choicevalue: content_choice //#endif //#if ALLOW_CHOICES // choices construct - choices: "{" [ choicesoptions_sampler | ( choicesoptions _WHITESPACE? "$$" ) ] choice ( "|" choice )* "}" + choices: "{" [ choicesoptions_sampler | ( choicesoptions "$$" ) ] choice ( "|" choice )* "}" //#endif //#if ALLOW_WILDCARDS // wildcard definition options - wcdefoptions: [ choicesoptions_sampler ] [ _WHITESPACE? choicesoptions_flags ] [ _WHITESPACE? choicesoptions_range ] [ _WHITESPACE? wcdescription ] [ _WHITESPACE? choicesoptions_sep ] + wcdefoptions: [ choicesoptions_sampler ] [ _WHITESPACE? choicesoptions_flags ] [ _WHITESPACE? choicesoptions_range ] [ _WHITESPACE? wcdescription ] [ _WHITESPACE? choicesoptions_sep ] "$$" wcdescription: STRING // wildcards construct - wildcard: "__" [ choicesoptions_sampler | ( choicesoptions _WHITESPACE? "$$" ) ] wildcard_name [ wc_filter ] [ wildcardvar ] "__" + wildcard: "__" [ choicesoptions_sampler | ( choicesoptions "$$" ) ] wildcard_name [ wc_filter ] [ wildcardvar ] "__" wc_filter_nums: IDENTIFIER | INDEX | (INDEX /-/ INDEX) //#if ALLOW_COMMVARS wildcard_name.2: ( WC_NAME_PLAIN_START | variableuse | commandecho ) ( WC_NAME_PLAIN | variableuse | commandecho )* diff --git a/ppp_tree.py b/ppp_tree.py index f9e28ca..9f15ef5 100644 --- a/ppp_tree.py +++ b/ppp_tree.py @@ -1891,8 +1891,12 @@ class TreeProcessor(lark.visitors.Interpreter): included_choices = 0 excluded_choices = 0 excluded_weights_sum = 0 + else_choice = None for i, c in enumerate(expanded_choice_values): c["choice_index"] = i # we index them to later sort the results + if c.get("else", False): + else_choice = c + continue weight = float(c.get("weight", 1.0)) condition = c.get("if", None) if weight > 0 and (condition is None or self.__eval_condition(condition)): @@ -1903,6 +1907,10 @@ class TreeProcessor(lark.visitors.Interpreter): weights.append(-1) excluded_choices += 1 excluded_weights_sum += weight + if not available_choices and else_choice is not None: + available_choices = [else_choice] + weights = [float(else_choice.get("weight", 1.0))] + included_choices = 1 if excluded_choices > 0: # we need to redistribute the excluded weights weights = [weight + excluded_weights_sum / included_choices for weight in weights if weight >= 0] weights = np.array(weights) @@ -2114,7 +2122,12 @@ class TreeProcessor(lark.visitors.Interpreter): c_label_obj = choice.children[1] choice_dict["labels"] = [str(x).lower() for x in c_label_obj.children[1:-1]] if c_label_obj is not None else [] choice_dict["weight"] = float(choice.children[2].children[0]) if choice.children[2] is not None else 1.0 - choice_dict["if"] = choice.children[3].children[0] if choice.children[3] is not None else None + ifelse = choice.children[3] + if ifelse is not None: + if len(ifelse.children) and ifelse.children[0] is not None: + choice_dict["if"] = ifelse.children[0] + else: + choice_dict["else"] = True choice_dict["content"] = choice.children[-1] return choice_dict @@ -2242,7 +2255,7 @@ class TreeProcessor(lark.visitors.Interpreter): parse_prompt( self.state, "as wildcard options", - wildcard.unprocessed_choices[0][:-2].strip(), + wildcard.unprocessed_choices[0], self.state.parsers["wcdefoptions"], True, ), diff --git a/ppp_wildcards.py b/ppp_wildcards.py index ea8f47c..44fdbd6 100644 --- a/ppp_wildcards.py +++ b/ppp_wildcards.py @@ -268,7 +268,7 @@ class PPPWildcards: Returns: bool: Whether the dictionary is a valid choice options dictionary or not. """ - return all(k in ["command", "labels", "weight", "if", "content", "text"] for k in d.keys()) + return all(k in ["command", "labels", "weight", "if", "else", "content", "text"] for k in d.keys()) def __get_choices(self, obj: object, full_path: Path | None, key_parts: list[str]) -> list: """ diff --git a/tests/tests_choices.py b/tests/tests_choices.py index 27b65c6..5e6abb4 100644 --- a/tests/tests_choices.py +++ b/tests/tests_choices.py @@ -103,6 +103,13 @@ class TestChoices(TestPromptPostProcessorBase): ppp="nocup", ) + def test_ch_choices_if_default(self): # choices with if and a default + self.process( + InputTuple("the choice is: {if false::choice1|if _is_sd1::choice2|else::choice3}", ""), + OutputTuple("the choice is: choice3", ""), + ppp="nocup", + ) + def test_ch_choices_set_if_multiple(self): # choices with if user variable and multiple selection self.process( InputTuple("${var=test}the choices are: {2$$, $$3::choice1|2 if not var eq 'test'::choice2|choice3}", ""), diff --git a/tests/tests_wildcards.py b/tests/tests_wildcards.py index ff62f4d..7558b17 100644 --- a/tests/tests_wildcards.py +++ b/tests/tests_wildcards.py @@ -337,6 +337,13 @@ class TestWildcards(TestPromptPostProcessorBase): ppp="nocup", ) + def test_wc_wildcard_emptydefault(self): # empty wildcard with default + self.process( + InputTuple("the choices are: __yaml/empty_default_wildcard__", ""), + OutputTuple("the choices are: 6", ""), + ppp="nocup", + ) + def test_wc_wildcard4_yaml(self): # simple yaml wildcard with one option self.process( InputTuple("the choices are: __yaml/wildcard4__", ""), diff --git a/tests/wildcards/test.yaml b/tests/wildcards/test.yaml index 52ccf8d..f28986e 100644 --- a/tests/wildcards/test.yaml +++ b/tests/wildcards/test.yaml @@ -111,6 +111,14 @@ yaml: - if _sd in ("test1", "test2")::4 - if (false or false)::5 + empty_default_wildcard: + - if false::1 + - if false::2 + - if false::3 + - if _sd in ("test1", "test2")::4 + - if (false or false)::5 + - else::6 + circular1: - 5::__yaml/circular2__ - choice1