Skip to content

mpppk/messagen

Repository files navigation

messagen

GoDoc

messagen is the tree-structured message generator with flexible constraints and declarative API. You can use messagen as a CLI tool or golang library.

Installation

CLI

Download from GitHub Releases.

golang library

$ go get github.com/mpppk/messagen

Getting Started

Think about a message that introducing someone else's name like He is Liam Smith. or She is Emily Williams., and you want to change pronoun and first/last name randomly. What about the following template?

{{.Pronoun}} is {{.FirstName}} {{.LastName}}.
# Pronoun is picked from ['He, 'She'] randomly.
# FirstName is picked from ['Liam', 'James', 'Emily', 'Charlotte', ...] randomly.
# LastName is picked from ['Smith', 'Williams', 'Brown'] randomly.

This template may work well but may generate inconsistent messages, because this message has one constraints. If the pronoun is He, the first name must be masculine. The same is true if the pronoun is She.

He is Liam Smith. # OK
She is Liam Smith. # NG because Liam is a masculine name

messagen is the tool for generating messages that satisfy the constraints between words by declarative API. messagen determines messages that can be generated by a set of definitions that group together templates, constraints, and others.

Below is the definitions of messagen written in YAML.

# intro.yaml
Definitions:
  - Type: Root
    Templates: ["{{.Pronoun}} is {{.FirstName}} {{.LastName}}."]
  - Type: Pronoun
    Templates: ["He", "She"]
  - Type: FirstName
    Templates: ["Liam", "James", "Benjamin"]
    Constraints: {"Pronoun": "He"}
  - Type: FirstName
    Templates: ["Emily", "Charlotte", "Sofia"]
    Constraints: {"Pronoun": "She"}
  - Type: LastName
    Templates: ["Smith", "Williams", "Brown"]

Then, execute messagen CLI.

$ messagen run -f intro.yaml
He is Liam Williams.

messagen always generates a consistent message.

Type is an identifier used for definition grouping. Multiple definitions can have same Type. Definition can be referenced by describing it as {{.SomeType}} in the template. If multiple definitions are found, one of them is picked at random. (In the golang library, this behavior can be controlled by DefinitionPicker. See pickers section.) Root is a special type that is the starting point for message generation.

Templates is a set of templates used for message generation. If a definition which have multiple templates is chosen, one of them is picked at random. (In the golang library, this behavior can be controlled by TemplatePicker. See pickers section)

Constraints is a key-value object that determines whether a definition is pickable. Key is a definition type. Value is a required definition value.

If template includes other definition types, messagen choose one of definition, then pick one of the templates, and these processes are repeated recursively. In other words, the definitions can be regarded as having the tree structure.

In above example, there is only one Root Definition with one template ("{{.Pronoun}} is {{.FirstName}} {{.LastName}}."). The template includes three definition types, Pronoun, FirstName, and LastName. This can be represented as the following tree structure:

state: {}

Root:          ['{{.Pronoun}} is {{.FirstName}} {{.LastName}}.']
├── Pronoun:   ['He', 'She']
├── FirstName: ['Liam', 'Emily', ...]
└── LastName:  ['Smith', 'Williams', 'Brown']

By default, definition types are resolved in order from the beginning of template, so Pronoun is resolved first in this example. If She is chosen as Pronoun, messagen state becomes as follows.

state: {Pronoun: She}

Root:          'She is {{.FirstName}} {{.LastName}}.'
├── Pronoun    ['He'] -- pick random --> 'She'
├── FirstName: ['Emily', 'Charlotte', 'Sofia'] (masculine name is dropped because unsatisfy constraints)
└── LastName:  ['Smith', 'Williams', 'Brown']

Next, FirstName is resolved.

state: {Pronoun: She, FirstName: Emily}

Root:          'She is Emily {{.LastName}}.'
├── Pronoun    ['He'] 
├── FirstName: ['Charlotte', 'Sofia'] -- pick random --> 'Emily'
└── LastName:  ['Smith', 'Williams', 'Brown']

Last, LastName is resolved, and messagen return the generated message.

state: {Pronoun: She, FirstName: Emily, LastName: Smith}

Root:          'She is Emily Smith.'
├── Pronoun    ['He'] 
├── FirstName: ['Charlotte', 'Sofia'] 
└── LastName:  ['Williams', 'Brown'] -- pick random --> 'Smith'
$ messagen run -f intro.yaml 
She is Emily Smith.

You can provide initial state by --state flag.

$ messagen run -f intro.yaml --state Pronoun=Male
He is Liam Williams.

golang sample

messagen can be used not only as a CLI tool but also as a golang library. Below is a sample of the previous definitions written in golang.

func main() {
   // CLI tool randomly picks a template by default, but in golang, you must specify it explicitly.
   opt := &messagen.Option{
      TemplatePickers: []messagen.TemplatePicker{messagen.RandomTemplatePicker},
    }
   generator, _ := messagen.New(opt)
)

   definitions := []*messagen.Definition{
      {
         Type: "Root",
         Templates: []string{"{{.Pronoun}} is {{.FirstName}} {{.LastName}}."},
      },
      {
         Type:      "Pronoun",
         Templates: []string{"He", "She"},
      },
      {
         Type:        "FirstName",
         Templates:   []string{"Liam", "James", "Benjamin"},
         Constraints: map[string]string{"Pronoun": "He"},
      },
      {
         Type:        "FirstName",
         Templates:   []string{"Emily", "Charlotte", "Sofia"},
         Constraints: map[string]string{"Pronoun": "She"},
      },
      {
         Type:      "LastName",
         Templates: []string{"Smith", "Williams", "Brown"},
      },
   }

   // AddDefinition definitions to generator.
   generator.AddDefinition(definitions...)

   // Set random seed for pick definitions and templates.
   rand.Seed(0)
    
    initialState := map[string]string{"Pronoun": "She"}

   // Generate method generate message according to added definitions.
   // First argument represent definition Type of start point.
   // Second argument represent initial state.
    // Third argument represent num of messages.
   messages, _ := generator.Generate("Root", initialState, 1)
    fmt.Println(messages[0]) 
    // output:
    // She is Emily.
}

Advanced tutorial

Constraints Operator

Constraints Operator is a special operation to constraint the checking process. It is expressed as symbols as a constraint key suffix, like SomeKey?/.

Here are available constraints operators list.

? operator means that this definition can be picked even if this constraint key does not exist.
+ operator is the same as ?, but add constraint key and value to state if they do not exist.
! operator means that this definition can be picked only if this constraint value is different from the value in the state.
/ operator means that this constraint value is evaluated as a regular expression.

Constraints Priority

The constraint priority is the priority for specifying the definition selection order according to the satisfied constraints. Constraint priority can be specified with colon and number in constraint key as suffix like SomeKey:1. If there are multiple definitions with the same definition type, messagen preferentially picks up the definition with the highest priority. The default priority is zero, so if the below definitions are provided, the first definition is always picked up because it has highest(1) constraint priority, and others have zero. If some definitions have the same priority, the order depends on the other definitions picker implementations.

 # This definition is always picked 
 # because it has highest(1) constraint priority.
 - Type: Root
    Templates: ["a"]
    Constraints: {"Key:1": "Value"}

# This definition has constraints, but any priority is not specified,
# so priority is zero.
 - Type: Root
    Templates: ["b"]
    Constraints: {"Key": "Value"}

# This definition has no constraint, so priority is zero. 
- Type: Root 
    Templates: ["c"]

# This definition has one constraint and has 1 priority, 
# but the state does not have OtherKey, so priority is zero.
 - Type: Root
    Templates: ["d"]
    Constraints: {"OtherKey?:1": "Value"}
$ messagen -f test.yaml --state Key=Value
a

Definition Resolution Order

By default, the definition types which contained in a template are resolved in the order they appear. For example, in the template like {{.A}} {{.B}} {{.C}}, the resolution order is A, B, C. You can change the order by set Order property to the definition like below.

Definitions:
  - Type: Root
    Templates: ["{{.A}} {{.B}} {{.C}} {{.D}}"]
    Order: ["B", "D"]
    # => definition types are picked up in the order of B, D, A, C

Definition Weight

By default, if multiple definitions have the same type, one of the definitions is picked randomly. By using Weight, you can control the probability that a definition will be picked. If Weight is omitted or a value less than or equal to 0 is specified, the weight is treated as 1.

In the below example, normal message has 1 weight, and rare message has 0.1 weight. The probabilities that the definition will be picked are as follows:

normal message probability = 1 / (1 + 0.1) ≒ 90.9%

rare message probability = 0.1 / (1 + 0.1) ≒ 9.1%

Definitions:
  - Type: Root
    Templates: ["normal message"]
  - Type: Root
    Templates: ["rare message"]
    Weight: 0.1

Alias

Sometimes you may want to pick up multiple definitions from the same Type.

The following example will have duplicate adjectives because the same definition is always picked up from the same type.

Definitions:
  - Type: Root
    Templates: ["messagen is a {{.Adjective}} and {{.Adjective}} message generator."]
  - Type: Adjective
    Templates: ["powerful", "user friendly", "minimal"]
$ messagen -f test.yaml
messagen is a powerful and powerful message generator.

By using an alias, you can retrieve different values from the same definition group.

Definitions:
  - Type: Root
    Templates: ["messagen is a {{.Adjective}} and {{.AnotherAdjective}} message generator."]
    Aliases:
      AnotherAdjective: {"Type": "Adjective", "AllowDuplicate": false}
  - Type: Adjective
    Templates: ["powerful", "user friendly", "minimal"]
$ messagen -f test.yaml
messagen is a minimal and powerful message generator.

golang tutorial

Here is a brief explanation. If you want to check more details, see godoc.

Definition

You can use messagen features like constraints, aliases, and others by create definition struct.

aliases := map[string]*Alias{
    "AnotherFirstName": &Alias{
        Type: "FirstName",
        AllowDuplicate: false,
    },
}

definition := Definition{
    Type:        "Root",
    Templates:   []string{"They are {{.FirstName}} and {{.AnotherFirstName}}."},
    Constraints: map[string]string{"Mode": "Introduce"},
    Aliases: aliases,
    Order: []string{},
    Weight: 0.5,
}

pickers

There are times when you want to do advanced message generation that cannot be handled with the built-in constraints system. You can use picker to apply user-defined constraints.

There are two types of picker: Definition picker and Template picker.

Definition picker

Definition picker is a function that determines the order in which definitions are picked when a definition type is given. Definition picker receives two arguments: Definitions and State, and returns the definition list. You can filter and rearrange the definitions, then return them.

type DefinitionPicker func(defs *Definitions, state *State) ([]*Definition, error)

Below is an implementation of a definition picker that is used inside messagen to check whether constraints are satisfied.

func ConstraintsSatisfiedDefinitionPicker(definitions *Definitions, state *State) ([]*Definition, error) {
   var newDefinitions Definitions
   for _, def := range *definitions {
      if ok, err := def.CanBePicked(state); err != nil {
         return nil, err
      } else if ok {
         newDefinitions = append(newDefinitions, def)
      }
   }
   return newDefinitions, nil
}

Template picker

Template picker is a function that determines the order in which templates are picked when a definition is picked. Template picker receives two arguments: DefinitionWithAlias and State, and returns Templates. You can filter and rearrange the templates, then return them.

type TemplatePicker func(def *DefinitionWithAlias, state *State) (Templates, error)

Below is an implementation of template picker that is used inside messagen to select a template randomly.

func RandomTemplatePicker(def *DefinitionWithAlias, state *State) (Templates, error) {
   templates := def.Templates
   var newTemplates Templates
   for {
      if len(templates) == 0 {
         break
      }
      tmpl, ok := templates.PopRandom()
      if !ok {
         return nil, xerrors.Errorf("failed to pop template randomly from %v", templates)
      }
      newTemplates = append(newTemplates, tmpl)
   }
   return newTemplates, nil
}

validator

The validator is a function that determines whether the current template and state are valid. Template validator receives two arguments: Template and State, then do your validation and returns as boolean.

type TemplateValidator = func(template *Template, state *State) (bool, error)

For example, if you want to post generated messages to twitter, a number of characters must be limited to 280. To cover such cases, messagen has MaxStrLenValidator.

func MaxStrLenValidator(maxLen int) func(template *Template, state *State) (bool, error) {
   return func(template *Template, state *State) (bool, error) {
      incompleteMsg, _, err := template.ExecuteWithIncompleteState(state)
      if err != nil {
         return false, err
      }
      return utf8.RuneCountInString(string(incompleteMsg)) <= maxLen, nil
   }
}

Pass pickers and validators to messagen

You can pass pickers and validators to messagen as messagen.Option.

   opt := &messagen.Option{
      TemplatePickers:    []messagen.TemplatePicker{messagen.RandomTemplatePicker, IrohaTemplatePicker},
      TemplateValidators: []messagen.TemplateValidator{IrohaTemplateValidator},
   }
   generator, err := messagen.New(opt)

About

Tree-structured message generator with flexible constraints by declarative API

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages