├── CHANGELOG.md ├── test ├── test_helper.exs ├── why_test.exs ├── discovery_test.exs └── retex_test.exs ├── .formatter.exs ├── lib ├── protocols │ ├── alpha_network.ex │ └── activation.ex ├── agenda │ ├── strategy_behaviour.ex │ └── execute_once.ex ├── discovery.ex ├── facts │ ├── relation.ex │ ├── filter.ex │ ├── isa.ex │ ├── is_not.ex │ ├── facts.ex │ ├── not_existing_attribute.ex │ └── has_attribute.ex ├── why.ex ├── nodes │ ├── root.ex │ ├── type_select_not.ex │ ├── negative_type_node.ex │ ├── type_select.ex │ ├── type_test.ex │ ├── p_node.ex │ ├── type_node.ex │ └── beta_memory.ex ├── token.ex ├── wme.ex ├── serializer.ex └── retex.ex ├── .github └── workflows │ └── elixir.yml ├── .gitignore ├── config └── config.exs ├── benchmark ├── rule_chain.exs └── example.exs ├── mix.exs ├── mix.lock ├── LICENSE └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # tbd -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /lib/protocols/alpha_network.ex: -------------------------------------------------------------------------------- 1 | defprotocol Retex.Protocol.AlphaNetwork do 2 | @doc "This protocol knows how append a new partial production into an existing network based on the type of fact" 3 | def append(fact, accumulator) 4 | end 5 | -------------------------------------------------------------------------------- /lib/agenda/strategy_behaviour.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Agenda.Strategy do 2 | @moduledoc false 3 | 4 | @type rules_executed :: list() 5 | @type network :: Retex.t() 6 | 7 | @callback consume_agenda(rules_executed, network) :: {:ok, {rules_executed, network}} 8 | end 9 | -------------------------------------------------------------------------------- /lib/protocols/activation.ex: -------------------------------------------------------------------------------- 1 | defprotocol Retex.Protocol.Activation do 2 | @doc "This protocol knows how to activate a node and pass the information to children" 3 | def activate(node, rete, wme, bindings, tokens) 4 | 5 | @doc "Implements the strategy to know if a node is activated or not" 6 | def active?(rete, node) 7 | end 8 | -------------------------------------------------------------------------------- /lib/discovery.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Discovery do 2 | @moduledoc """ 3 | Checks which production nodes have been almost activated from the ones 4 | with the most activated paths pointing to them, sorted by weak activations. 5 | """ 6 | 7 | defstruct weak_activations: [] 8 | 9 | def weak_activations(%Retex{graph: _graph}) do 10 | %__MODULE__{weak_activations: []} 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/facts/relation.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Fact.Relation do 2 | @moduledoc "Attribute values that a Wme should have in order for this condition to be true" 3 | defstruct name: nil, from: nil, to: nil, via: nil 4 | 5 | def new(fields) do 6 | via = to_string(fields[:from]) |> String.downcase() |> Kernel.<>("_id") 7 | rel = struct(__MODULE__, fields) 8 | Map.put(rel, :via, via) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | container: 10 | image: elixir:1.13.1 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Install Dependencies 15 | run: | 16 | mix local.rebar --force 17 | mix local.hex --force 18 | mix deps.get 19 | - name: Run Tests 20 | run: mix test 21 | -------------------------------------------------------------------------------- /lib/why.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Why do 2 | @moduledoc """ 3 | Why a rule was activated? 4 | 5 | Use this module passing the conclusion of a rule and it will tell you why it was activated 6 | """ 7 | 8 | defstruct conclusion: nil, paths: [] 9 | alias Retex.Node.PNode 10 | 11 | def explain(%Retex{graph: graph}, %PNode{} = conclusion) do 12 | conclusion = PNode.new(conclusion.raw_action) 13 | paths = Graph.get_paths(graph, Retex.root_vertex(), conclusion) 14 | %__MODULE__{paths: paths, conclusion: conclusion} 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/facts/filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Fact.Filter do 2 | @moduledoc "Apply a filter to a variable on the PNode, so that the activation of a PNode happens when this condition is also satisfied" 3 | defstruct variable: nil, predicate: nil, value: nil 4 | 5 | @type variable :: String.t() 6 | @type predicate :: :== | :=== | :!== | :!= | :> | :< | :<= | :>= | :in 7 | @type value :: any() 8 | @type t :: %__MODULE__{variable: variable(), predicate: predicate(), value: value()} 9 | @type fields :: [variable: variable(), predicate: predicate(), value: value()] 10 | 11 | @spec new(fields()) :: t() 12 | def new(fields) do 13 | struct(__MODULE__, fields) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/nodes/root.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Root do 2 | @moduledoc """ 3 | The root node is the root vertex of the network. 4 | From the root node start all the edges that connect with each TypeNode, 5 | the discrimination network starts from here. 6 | """ 7 | defstruct class: :Root, id: :root 8 | 9 | @type t :: %Retex.Root{} 10 | 11 | def new, do: %__MODULE__{} 12 | 13 | defimpl Retex.Protocol.Activation do 14 | def activate( 15 | %Retex.Root{} = neighbor, 16 | %Retex{graph: _graph} = rete, 17 | %Retex.Wme{attribute: _attribute} = wme, 18 | bindings, 19 | _tokens 20 | ) do 21 | Retex.continue_traversal(rete, bindings, neighbor, wme) 22 | end 23 | 24 | def active?(_node, _rete), do: true 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | rete_elixir-*.tar 24 | 25 | # Ignore elixir language server cache 26 | .elixir_ls/ 27 | 28 | # DStore 29 | .DS_Store 30 | 31 | 32 | /priv/plts/* -------------------------------------------------------------------------------- /test/why_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Retex.WhyTest do 2 | use ExUnit.Case 3 | alias Retex.Facts 4 | import Facts 5 | alias Retex.Why 6 | 7 | defp create_rule(lhs: given, rhs: action) do 8 | %{ 9 | given: given, 10 | then: action 11 | } 12 | end 13 | 14 | test "two paths are activated in order to reach a conclusion" do 15 | given = [ 16 | has_attribute(:Account, :status, :==, "$a"), 17 | has_attribute(:Family, :size, :==, "$a") 18 | ] 19 | 20 | wme = Retex.Wme.new(:Account, :status, :ok) 21 | wme_2 = Retex.Wme.new(:Family, :size, :ok) 22 | 23 | action = [ 24 | {:Flight, :account_status, "$a"} 25 | ] 26 | 27 | rule = create_rule(lhs: given, rhs: action) 28 | 29 | network = 30 | Retex.add_production(Retex.new(), rule) |> Retex.add_wme(wme) |> Retex.add_wme(wme_2) 31 | 32 | action = network.agenda |> List.first() 33 | 34 | assert 2 == Enum.count(Why.explain(network, action) |> Map.get(:paths)) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/facts/isa.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Fact.Isa do 2 | @moduledoc "A type of thing that needs to exists in order for a Wme to activate part of a condition" 3 | defstruct type: nil, variable: nil 4 | 5 | @type type :: String.t() | atom() 6 | @type variable :: String.t() 7 | @type fields :: [type: type(), variable: variable()] 8 | @type t :: %__MODULE__{type: type(), variable: variable()} 9 | 10 | @spec new(fields()) :: t() 11 | def new(fields) do 12 | struct(__MODULE__, fields) 13 | end 14 | 15 | defimpl Retex.Protocol.AlphaNetwork do 16 | alias Retex.Fact.Isa 17 | alias Retex.Node.Type 18 | 19 | def append(%Isa{} = condition, {graph, test_nodes}) do 20 | %{variable: _, type: type} = condition 21 | type_node = Type.new(type) 22 | 23 | new_graph = 24 | graph 25 | |> Graph.add_vertex(type_node) 26 | |> Graph.add_edge(Retex.root_vertex(), type_node) 27 | 28 | {new_graph, [type_node | test_nodes]} 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/discovery_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Retex.DiscoveryTest do 2 | use ExUnit.Case 3 | alias Retex.Facts 4 | import Facts 5 | alias Retex.Why 6 | 7 | defp create_rule(lhs: given, rhs: action) do 8 | %{ 9 | given: given, 10 | then: action 11 | } 12 | end 13 | 14 | test "two paths are activated in order to reach a conclusion" do 15 | given = [ 16 | has_attribute(:Account, :status, :==, "$a"), 17 | has_attribute(:Family, :size, :==, "$a") 18 | ] 19 | 20 | wme = Retex.Wme.new(:Account, :status, :ok) 21 | wme_2 = Retex.Wme.new(:Family, :size, :ok) 22 | 23 | action = [ 24 | {:Flight, :account_status, "$a"} 25 | ] 26 | 27 | rule = create_rule(lhs: given, rhs: action) 28 | 29 | network = 30 | Retex.add_production(Retex.new(), rule) |> Retex.add_wme(wme) |> Retex.add_wme(wme_2) 31 | 32 | action = network.agenda |> List.first() 33 | assert 2 == Enum.count(Why.explain(network, action) |> Map.get(:paths)) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/facts/is_not.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Fact.IsNot do 2 | @moduledoc "A type of thing that needs to not exists in order for a Wme to activate part of a condition" 3 | defstruct type: nil, variable: nil 4 | 5 | @type type :: String.t() | atom() 6 | @type variable :: String.t() 7 | @type fields :: [type: type(), variable: variable()] 8 | @type t :: %__MODULE__{type: type(), variable: variable()} 9 | 10 | @spec new(fields()) :: t() 11 | def new(fields) do 12 | struct(__MODULE__, fields) 13 | end 14 | 15 | defimpl Retex.Protocol.AlphaNetwork do 16 | alias Retex.Fact.IsNot 17 | alias Retex.Node.NegativeType 18 | 19 | def append(%IsNot{} = condition, {graph, test_nodes}) do 20 | %{variable: _, type: type} = condition 21 | type_node = NegativeType.new(type) 22 | 23 | new_graph = 24 | graph 25 | |> Graph.add_vertex(type_node) 26 | |> Graph.add_edge(Retex.root_vertex(), type_node) 27 | 28 | {new_graph, [type_node | test_nodes]} 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/token.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Token do 2 | @moduledoc """ 3 | The root node of the network is the input to the black box. 4 | This node receives the tokens that are sent to the black box and passes copies 5 | of the tokens to all its successors. The successors of the top node, the nodes to 6 | perform the intra-element tests, have one input and one or more outputs. Each 7 | node tests one feature and sends the tokens that pass the test to its successors. 8 | The two-input nodes compare tokens from different paths and join them into 9 | bigger tokens if they satisfy the inter-element constraints of the LHS. Because 10 | of the tests performed by the other nodes, a terminal node will receive only 11 | tokens that instantiate the LHS. The terminal node sends out of the black box 12 | the information that the conflict set must be changed. - RETE Match Algorithm - Forgy OCR 13 | """ 14 | defstruct wmem: nil, node: nil, bindings: %{} 15 | 16 | @type t() :: %Retex.Token{} 17 | 18 | def new do 19 | %__MODULE__{} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/wme.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Wme do 2 | @moduledoc """ 3 | A working memory element, it represent the world in the form of identifier, attribute and values 4 | timestamp is set at time of insertion into retex 5 | """ 6 | @type wme_identifier() :: String.t() | atom() 7 | @type attribute() :: String.t() | atom() 8 | @type id() :: String.t() | number() 9 | @type value() :: any() 10 | 11 | @type t :: %__MODULE__{ 12 | identifier: wme_identifier(), 13 | attribute: attribute(), 14 | id: id(), 15 | timestamp: number(), 16 | value: value() 17 | } 18 | 19 | defstruct identifier: nil, attribute: nil, value: nil, id: nil, timestamp: nil 20 | alias Retex.Fact.HasAttribute 21 | 22 | def new(id, attr, val) do 23 | item = %__MODULE__{identifier: id, attribute: attr, value: val} 24 | Map.put(item, :id, Retex.hash(item)) 25 | end 26 | 27 | def to_has_attribute(%Retex.Wme{} = wme) do 28 | %__MODULE__{identifier: id, attribute: attr, value: val} = wme 29 | HasAttribute.new(owner: id, attribute: attr, predicate: :==, value: val) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/agenda/execute_once.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Agenda.ExecuteOnce do 2 | @moduledoc false 3 | @behaviour Retex.Agenda.Strategy 4 | 5 | def consume_agenda(executed_rules, %{agenda: [rule | rest] = _agenda} = network) do 6 | {rule_id, network} = execute_rule(rule, network) 7 | 8 | executed_rules = [rule_id] ++ executed_rules 9 | 10 | next_rules = 11 | network.agenda 12 | |> Enum.reject(fn pnode -> 13 | Enum.member?(executed_rules, pnode.id) 14 | end) 15 | 16 | network = 17 | if Enum.empty?(next_rules) do 18 | network 19 | else 20 | consume_agenda(executed_rules, %{network | agenda: next_rules ++ rest}) 21 | end 22 | 23 | {executed_rules, network} 24 | end 25 | 26 | def execute_rule(%{id: id, raw_action: raw_action} = _rule, network) do 27 | new_network = 28 | Enum.reduce(raw_action, network, fn action, network -> 29 | do_execute_action(action, network) 30 | end) 31 | 32 | {id, new_network} 33 | end 34 | 35 | defp do_execute_action(%Retex.Wme{} = wme, network) do 36 | Retex.add_wme(network, wme) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | import Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # third-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :retex, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:retex, :key) 18 | # 19 | # You can also configure a third-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /lib/facts/facts.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Facts do 2 | @moduledoc false 3 | alias Retex.Fact 4 | 5 | @type t :: Fact.Relation.t() | Fact.Isa.t() | Fact.HasAttribute.t() 6 | 7 | def relation(from, name, to) do 8 | Fact.Relation.new(name: name, from: from, to: to) 9 | end 10 | 11 | def isa(variable, type) do 12 | isa(variable: variable, type: type) 13 | end 14 | 15 | def is_not(variable, type) do 16 | is_not(variable: variable, type: type) 17 | end 18 | 19 | def filter(variable, predicate, value) do 20 | Fact.Filter.new(variable: variable, predicate: predicate, value: value) 21 | end 22 | 23 | def isa(fields) do 24 | Fact.Isa.new(fields) 25 | end 26 | 27 | def is_not(fields) do 28 | Fact.IsNot.new(fields) 29 | end 30 | 31 | def has_attribute(owner, attribute, predicate, value) do 32 | has_attribute(owner: owner, attribute: attribute, predicate: predicate, value: value) 33 | end 34 | 35 | def has_attribute(fields) do 36 | Fact.HasAttribute.new(fields) 37 | end 38 | 39 | def not_existing_attribute(owner, attribute) do 40 | not_existing_attribute(owner: owner, attribute: attribute) 41 | end 42 | 43 | def not_existing_attribute(fields) do 44 | Fact.NotExistingAttribute.new(fields) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/facts/not_existing_attribute.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Fact.NotExistingAttribute do 2 | @moduledoc "Attribute values that a Wme should NOT have in order for this condition to be true" 3 | 4 | defstruct owner: nil, attribute: nil 5 | 6 | @type owner :: String.t() | atom() 7 | @type attribute :: String.t() | atom() 8 | @type fields :: [owner: owner(), attribute: attribute()] 9 | 10 | @type t :: %Retex.Fact.NotExistingAttribute{ 11 | owner: owner(), 12 | attribute: attribute() 13 | } 14 | @spec new(fields()) :: t() 15 | def new(fields) do 16 | struct(__MODULE__, fields) 17 | end 18 | 19 | defimpl Retex.Protocol.AlphaNetwork do 20 | alias Retex.{Fact, Node} 21 | 22 | def append(%Fact.NotExistingAttribute{} = condition, {graph, nodes}) do 23 | %{attribute: attribute, owner: class} = condition 24 | type_node = Node.Type.new(class) 25 | negative_select_node = Node.SelectNot.new(class, attribute) 26 | 27 | new_graph = 28 | graph 29 | |> Graph.add_vertex(type_node) 30 | |> Graph.add_edge(Retex.root_vertex(), type_node) 31 | |> Graph.add_vertex(negative_select_node) 32 | |> Graph.add_edge(type_node, negative_select_node) 33 | 34 | {new_graph, [negative_select_node | nodes]} 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/nodes/type_select_not.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Node.SelectNot do 2 | @moduledoc """ 3 | The select nodes are checking for attributes, if they do NOT exists and are linked to the 4 | right owner from above, they will be activated and pass the tokens down 5 | """ 6 | defstruct class: nil, id: nil, bindings: %{}, parent: nil 7 | @type t() :: %Retex.Node.SelectNot{} 8 | 9 | def new(parent, class) do 10 | item = %__MODULE__{class: class, parent: parent} 11 | %{item | id: Retex.hash(item)} 12 | end 13 | 14 | defimpl Retex.Protocol.Activation do 15 | def activate( 16 | %Retex.Node.SelectNot{class: attribute} = neighbor, 17 | %Retex{} = rete, 18 | %Retex.Wme{attribute: attribute} = wme, 19 | bindings, 20 | tokens 21 | ) do 22 | rete 23 | |> Retex.create_activation(neighbor, wme) 24 | |> Retex.add_token(neighbor, wme, bindings, tokens) 25 | |> Retex.deactivate_descendants(neighbor) 26 | |> Retex.stop_traversal(%{}) 27 | end 28 | 29 | def activate( 30 | %Retex.Node.SelectNot{class: _class} = _neighbor, 31 | %Retex{} = rete, 32 | %Retex.Wme{attribute: _attribute}, 33 | bindings, 34 | _tokens 35 | ) do 36 | Retex.stop_traversal(rete, bindings) 37 | end 38 | 39 | @spec active?(%{id: any}, Retex.t()) :: boolean() 40 | def active?(%{id: id}, %Retex{activations: activations}) do 41 | Enum.empty?(Map.get(activations, id, [])) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/nodes/negative_type_node.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Node.NegativeType do 2 | @moduledoc """ 3 | The NegativeNodeType if part of the alpha network, the discrimination part of the network 4 | that check if a specific class DOES NOT exist. If this is the case, it propagates the activations 5 | down to the select node types. They will select an attribute and check for its test to pass. 6 | """ 7 | defstruct class: nil, id: nil 8 | @type t :: %Retex.Node.Type{} 9 | 10 | def new(class) do 11 | item = %__MODULE__{class: class} 12 | %{item | id: Retex.hash(item)} 13 | end 14 | 15 | defimpl Retex.Protocol.Activation do 16 | def activate( 17 | %Retex.Node.NegativeType{class: identifier} = neighbor, 18 | %Retex{} = rete, 19 | %Retex.Wme{identifier: identifier} = wme, 20 | bindings, 21 | tokens 22 | ) do 23 | rete 24 | |> Retex.create_activation(neighbor, wme) 25 | |> Retex.add_token(neighbor, wme, bindings, tokens) 26 | |> Retex.deactivate_descendants(neighbor) 27 | |> Retex.stop_traversal(%{}) 28 | end 29 | 30 | def activate( 31 | %Retex.Node.NegativeType{class: _class}, 32 | %Retex{graph: _graph} = rete, 33 | %Retex.Wme{identifier: _identifier} = _wme, 34 | _bindings, 35 | _tokens 36 | ) do 37 | Retex.stop_traversal(rete, %{}) 38 | end 39 | 40 | @spec active?(%{id: any}, Retex.t()) :: boolean() 41 | def active?(%{id: id}, %Retex{activations: activations}) do 42 | Enum.empty?(Map.get(activations, id, [])) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/facts/has_attribute.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Fact.HasAttribute do 2 | @moduledoc "Attribute values that a Wme should have in order for this condition to be true" 3 | 4 | defstruct owner: nil, attribute: nil, predicate: nil, value: nil 5 | 6 | @type owner :: String.t() | atom() 7 | @type attribute :: String.t() | atom() 8 | @type predicate :: :== | :=== | :!== | :!= | :> | :< | :<= | :>= | :in 9 | @type value :: any() 10 | @type fields :: [owner: owner(), attribute: attribute(), predicate: predicate(), value: value()] 11 | 12 | @type t :: %Retex.Fact.HasAttribute{ 13 | owner: owner(), 14 | attribute: attribute(), 15 | predicate: predicate(), 16 | value: value() 17 | } 18 | @spec new(fields()) :: t() 19 | def new(fields) do 20 | struct(__MODULE__, fields) 21 | end 22 | 23 | defimpl Retex.Protocol.AlphaNetwork do 24 | alias Retex.{Fact.HasAttribute, Node} 25 | 26 | def append(%HasAttribute{} = condition, {graph, test_nodes}) do 27 | %{attribute: attribute, owner: class, predicate: predicate, value: value} = condition 28 | condition_id = Retex.hash(condition) 29 | type_node = Node.Type.new(class) 30 | select_node = Node.Select.new(class, attribute) 31 | test_node = Node.Test.new([predicate, value], condition_id) 32 | 33 | new_graph = 34 | graph 35 | |> Graph.add_vertex(type_node) 36 | |> Graph.add_edge(Retex.root_vertex(), type_node) 37 | |> Graph.add_vertex(select_node) 38 | |> Graph.add_edge(type_node, select_node) 39 | |> Graph.add_vertex(test_node) 40 | |> Graph.add_edge(select_node, test_node) 41 | 42 | {new_graph, [test_node | test_nodes]} 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/nodes/type_select.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Node.Select do 2 | @moduledoc """ 3 | The select nodes are checking for attributes, if they exists and are linked to the 4 | right owner from above, they will be activated and pass the tokens down in the test 5 | nodes (that will check for their value instead). 6 | """ 7 | defstruct class: nil, id: nil, parent: nil 8 | @type t() :: %Retex.Node.Select{} 9 | 10 | def new(parent, class) do 11 | item = %__MODULE__{class: class, parent: parent} 12 | %{item | id: Retex.hash(item)} 13 | end 14 | 15 | defimpl Retex.Protocol.Activation do 16 | def activate( 17 | %Retex.Node.Select{class: "$" <> variable} = neighbor, 18 | %Retex{} = rete, 19 | %Retex.Wme{attribute: attribute} = wme, 20 | bindings, 21 | tokens 22 | ) do 23 | rete 24 | |> Retex.create_activation(neighbor, wme) 25 | |> Retex.add_token(neighbor, wme, Map.merge(bindings, %{variable => attribute}), tokens) 26 | |> Retex.continue_traversal(Map.merge(bindings, %{variable => attribute}), neighbor, wme) 27 | end 28 | 29 | def activate( 30 | %Retex.Node.Select{class: attribute} = neighbor, 31 | %Retex{} = rete, 32 | %Retex.Wme{attribute: attribute} = wme, 33 | bindings, 34 | tokens 35 | ) do 36 | rete 37 | |> Retex.create_activation(neighbor, wme) 38 | |> Retex.add_token(neighbor, wme, bindings, tokens) 39 | |> Retex.continue_traversal(bindings, neighbor, wme) 40 | end 41 | 42 | def activate( 43 | %Retex.Node.Select{class: _class} = _neighbor, 44 | %Retex{} = rete, 45 | %Retex.Wme{attribute: _attribute}, 46 | bindings, 47 | _tokens 48 | ) do 49 | Retex.stop_traversal(rete, bindings) 50 | end 51 | 52 | @spec active?(%{id: any}, Retex.t()) :: boolean() 53 | def active?(%{id: id}, %Retex{activations: activations}) do 54 | Enum.any?(Map.get(activations, id, [])) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/nodes/type_test.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Node.Test do 2 | @moduledoc """ 3 | Test node. 4 | 5 | If we reached that node, means that we are checking that a value is matching 6 | a specific condition. If this is the case, we activate this node and pass the token down 7 | to the beta network. 8 | """ 9 | defstruct class: nil, id: nil 10 | @type t :: %Retex.Node.Test{} 11 | 12 | def new(class, id), do: %__MODULE__{class: class, id: id} 13 | 14 | defimpl Retex.Protocol.Activation do 15 | def activate( 16 | %Retex.Node.Test{class: [_operator, "$" <> _variable = var]} = neighbor, 17 | %Retex{activations: _activations} = rete, 18 | %{value: value} = wme, 19 | bindings, 20 | tokens 21 | ) do 22 | rete 23 | |> Retex.create_activation(neighbor, wme) 24 | |> Retex.add_token(neighbor, wme, Map.merge(bindings, %{var => value}), tokens) 25 | |> Retex.continue_traversal(Map.merge(bindings, %{var => value}), neighbor, wme) 26 | end 27 | 28 | def activate( 29 | %Retex.Node.Test{class: [:in, value]} = neighbor, 30 | %Retex{activations: _activations} = rete, 31 | wme, 32 | bindings, 33 | tokens 34 | ) do 35 | if Enum.member?(value, wme.value) do 36 | rete 37 | |> Retex.create_activation(neighbor, wme) 38 | |> Retex.add_token(neighbor, wme, bindings, tokens) 39 | |> Retex.continue_traversal(bindings, neighbor, wme) 40 | else 41 | Retex.stop_traversal(rete, %{}) 42 | end 43 | end 44 | 45 | def activate( 46 | %Retex.Node.Test{class: [operator, value]} = neighbor, 47 | %Retex{activations: _activations} = rete, 48 | wme, 49 | bindings, 50 | tokens 51 | ) do 52 | if apply(Kernel, operator, [wme.value, value]) do 53 | rete 54 | |> Retex.create_activation(neighbor, wme) 55 | |> Retex.add_token(neighbor, wme, bindings, tokens) 56 | |> Retex.continue_traversal(bindings, neighbor, wme) 57 | else 58 | Retex.stop_traversal(rete, %{}) 59 | end 60 | end 61 | 62 | @spec active?(%{id: any}, Retex.t()) :: boolean() 63 | def active?(%{id: id}, %Retex{activations: activations}) do 64 | Enum.any?(Map.get(activations, id, [])) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /benchmark/rule_chain.exs: -------------------------------------------------------------------------------- 1 | defmodule Benchmark do 2 | alias Retex.Agenda 3 | 4 | defp has_attribute(owner, attribute, predicate, value) do 5 | has_attribute(owner: owner, attribute: attribute, predicate: predicate, value: value) 6 | end 7 | 8 | defp has_attribute(fields) do 9 | Retex.Fact.HasAttribute.new(fields) 10 | end 11 | 12 | defp create_rule(lhs: given, rhs: action, id: id) do 13 | %{ 14 | given: given, 15 | id: id, 16 | then: action 17 | } 18 | end 19 | 20 | def generate_rule_chain(depth) do 21 | for level <- 1..depth do 22 | given = [ 23 | has_attribute("Thing_#{level}", "attribute_#{level}", :==, level), 24 | has_attribute("Thing_#{level}", "attribute_#{level}", :!=, false) 25 | ] 26 | 27 | then = 28 | if level + 1 >= depth do 29 | [] 30 | else 31 | [Retex.Wme.new("Thing_#{level + 1}", "attribute_#{level + 1}", level + 1)] 32 | end 33 | 34 | create_rule(lhs: given, rhs: then, id: depth) 35 | end 36 | end 37 | 38 | def run(depth \\ 20000) do 39 | depth = 40 | case parse(System.argv()) do 41 | {[depth: depth], _, _} -> depth 42 | _ -> depth 43 | end 44 | 45 | rules = generate_rule_chain(depth) 46 | require Logger 47 | 48 | Logger.info("Adding #{depth} rules...") 49 | 50 | {time, rete_engine} = 51 | :timer.tc(fn -> 52 | Enum.reduce(rules, Retex.new(), fn rule, network -> 53 | Retex.add_production(network, rule) 54 | end) 55 | end) 56 | 57 | Graph.info(rete_engine.graph) |> inspect() |> Logger.info() 58 | Logger.info("Adding #{depth} rules took #{humanized_duration(time)}") 59 | 60 | wme = Retex.Wme.new("Thing_1", "attribute_1", 1) 61 | 62 | {time, rete_engine} = :timer.tc(fn -> Retex.add_wme(rete_engine, wme) end) 63 | 64 | Agenda.ExecuteOnce.consume_agenda([], rete_engine) 65 | 66 | Logger.info("Adding the working memory took #{humanized_duration(time)}") 67 | end 68 | 69 | defp parse(args) do 70 | OptionParser.parse(args, strict: [depth: :integer]) 71 | end 72 | 73 | defp humanized_duration(time) do 74 | duration_in_seconds = time / 1_000_000 75 | 76 | duration_in_seconds 77 | |> Timex.Duration.from_seconds() 78 | |> Timex.Format.Duration.Formatter.format(:humanized) 79 | end 80 | end 81 | 82 | Benchmark.run() 83 | -------------------------------------------------------------------------------- /lib/nodes/p_node.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Node.PNode do 2 | @moduledoc """ 3 | Production node. This is like a production node in Rete algorithm. It is activated if all 4 | the conditions in a rule are matching and contains the action that can be executed as consequence. 5 | """ 6 | 7 | defstruct action: nil, id: nil, raw_action: nil, bindings: %{}, filters: [] 8 | 9 | @type t() :: %__MODULE__{} 10 | 11 | def new(action, filters \\ []) do 12 | item = %__MODULE__{action: action, raw_action: action, filters: filters} 13 | %{item | id: Retex.hash(item)} 14 | end 15 | 16 | defimpl Retex.Protocol.Activation do 17 | def activate( 18 | %{filters: filters} = neighbor, 19 | %Retex{tokens: tokens, graph: graph, activations: activations} = rete, 20 | _wme, 21 | _bindings, 22 | _tokens 23 | ) do 24 | [parent] = 25 | parents = 26 | Graph.in_neighbors(graph, neighbor) 27 | |> Enum.filter(fn node -> 28 | Map.get(activations, node.id) 29 | end) 30 | 31 | tokens = Map.get(tokens, parent.id) 32 | 33 | if Enum.all?(parents, &Map.get(activations, &1.id)) do 34 | bindings = tokens |> Enum.reduce(%{}, fn t, acc -> Map.merge(acc, t.bindings) end) 35 | productions = apply_filters([Retex.replace_bindings(neighbor, bindings)], filters) 36 | 37 | new_rete = %{ 38 | rete 39 | | agenda: [productions | rete.agenda] |> List.flatten() |> Enum.uniq() 40 | } 41 | 42 | Retex.stop_traversal(new_rete, %{}) 43 | else 44 | Retex.stop_traversal(rete, %{}) 45 | end 46 | end 47 | 48 | def active?(_, _) do 49 | raise "Not implemented" 50 | end 51 | 52 | def apply_filters(nodes, filters) do 53 | Enum.filter(nodes, fn node -> pass_test?(filters, node) end) 54 | end 55 | 56 | defp pass_test?(filters, node) do 57 | Enum.reduce_while(filters, true, fn filter, _ -> 58 | if test_pass?(node, filter), do: {:cont, true}, else: {:halt, false} 59 | end) 60 | end 61 | 62 | def test_pass?(%Retex.Node.PNode{bindings: bindings}, %Retex.Fact.Filter{ 63 | predicate: predicate, 64 | value: value, 65 | variable: variable 66 | }) do 67 | case Map.get(bindings, variable, :_undefined) do 68 | :_undefined -> true 69 | current_value -> apply(Kernel, predicate, [current_value, value]) 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/serializer.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Serializer do 2 | @moduledoc """ 3 | This serializer converts a `Graph` to a [Mermaid Flowchart](https://mermaid.js.org/syntax/flowchart.html). 4 | """ 5 | 6 | use Graph.Serializer 7 | import Graph.Serializer 8 | 9 | @impl Graph.Serializer 10 | def serialize(%Graph{} = g) do 11 | result = """ 12 | flowchart 13 | #{serialize_vertices(g)} 14 | #{serialize_edges(g)} 15 | """ 16 | 17 | {:ok, result} 18 | end 19 | 20 | def vertex_labels(%Graph{vertex_labels: vl}, id, v) do 21 | case Map.get(vl, id) do 22 | [] -> encode(v) 23 | label -> encode(label) 24 | end 25 | end 26 | 27 | defp serialize_vertices(g) do 28 | Enum.map_join(g.vertices, "\n", fn {id, value} -> 29 | indent(1) <> "#{id}" <> "[\"" <> vertex_labels(g, id, value) <> "\"]" 30 | end) 31 | end 32 | 33 | defp serialize_edges(g) do 34 | arrow = 35 | case g.type do 36 | :directed -> "->" 37 | :undirected -> "-" 38 | end 39 | 40 | edges = 41 | g.vertices 42 | |> Enum.reduce([], fn {id, _}, acc -> 43 | out_edges = 44 | g.out_edges 45 | |> Map.get_lazy(id, &MapSet.new/0) 46 | |> Enum.flat_map(&fetch_edge(g, id, &1)) 47 | 48 | acc ++ out_edges 49 | end) 50 | 51 | Enum.map_join(edges, "\n", &serialize_edge(&1, arrow)) 52 | end 53 | 54 | defp fetch_edge(g, id, out_edge_id) do 55 | g.edges 56 | |> Map.fetch!({id, out_edge_id}) 57 | |> Enum.map(fn 58 | {nil, weight} -> {id, out_edge_id, weight} 59 | {label, weight} -> {id, out_edge_id, weight, encode(label)} 60 | end) 61 | end 62 | 63 | defp encode(%{class: name}) when is_list(name) do 64 | Enum.join(name) 65 | end 66 | 67 | defp encode(%{class: name}) do 68 | to_string(name) 69 | end 70 | 71 | defp encode(%Retex.Node.BetaMemory{}) do 72 | "Join" 73 | end 74 | 75 | defp encode(%Retex.Node.PNode{id: _id, action: action}) do 76 | "#{inspect(action, pretty: true)}" 77 | end 78 | 79 | defp serialize_edge({id, out_edge_id, weight, label}, arrow) do 80 | indent(1) <> "#{id} " <> weight_arrow(arrow, weight) <> " |#{label}| " <> "#{out_edge_id}" 81 | end 82 | 83 | defp serialize_edge({id, out_edge_id, weight}, arrow) do 84 | indent(1) <> "#{id} " <> weight_arrow(arrow, weight) <> " #{out_edge_id}" 85 | end 86 | 87 | defp weight_arrow(arrow, weight) do 88 | String.duplicate("-", weight) <> arrow 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/nodes/type_node.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Node.Type do 2 | @moduledoc """ 3 | The NodeType if part of the alpha network, the discrimination part of the network 4 | that check if a specific class exists. If this is the case, it propagates the activations 5 | down to the select node types. They will select an attribute and check for its existance. 6 | """ 7 | defstruct class: nil, id: nil 8 | @type t :: %Retex.Node.Type{} 9 | 10 | def new(class) do 11 | item = %__MODULE__{class: class} 12 | %{item | id: Retex.hash(item)} 13 | end 14 | 15 | defimpl Retex.Protocol.Activation do 16 | def activate( 17 | %Retex.Node.Type{class: class} = neighbor, 18 | %Retex{graph: _graph} = rete, 19 | %Retex.Wme{identifier: "$" <> _identifier = var} = wme, 20 | bindings, 21 | tokens 22 | ) do 23 | new_bindings = Map.merge(bindings, %{var => class}) 24 | 25 | rete 26 | |> Retex.create_activation(neighbor, wme) 27 | |> Retex.add_token(neighbor, wme, new_bindings, tokens) 28 | |> Retex.continue_traversal(new_bindings, neighbor, wme) 29 | end 30 | 31 | def activate( 32 | %Retex.Node.Type{class: "$" <> _variable = var} = neighbor, 33 | %Retex{graph: _graph} = rete, 34 | %Retex.Wme{identifier: identifier} = wme, 35 | bindings, 36 | tokens 37 | ) do 38 | rete 39 | |> Retex.create_activation(neighbor, wme) 40 | |> Retex.add_token(neighbor, wme, Map.merge(bindings, %{var => identifier}), tokens) 41 | |> Retex.continue_traversal(Map.merge(bindings, %{var => identifier}), neighbor, wme) 42 | end 43 | 44 | def activate( 45 | %Retex.Node.Type{class: identifier} = neighbor, 46 | %Retex{} = rete, 47 | %Retex.Wme{identifier: identifier} = wme, 48 | bindings, 49 | tokens 50 | ) do 51 | rete 52 | |> Retex.create_activation(neighbor, wme) 53 | |> Retex.add_token(neighbor, wme, bindings, tokens) 54 | |> Retex.continue_traversal(bindings, neighbor, wme) 55 | end 56 | 57 | def activate( 58 | %Retex.Node.Type{class: _class}, 59 | %Retex{graph: _graph} = rete, 60 | %Retex.Wme{identifier: _identifier} = _wme, 61 | _bindings, 62 | _tokens 63 | ) do 64 | Retex.stop_traversal(rete, %{}) 65 | end 66 | 67 | @spec active?(%{id: any}, Retex.t()) :: boolean() 68 | def active?(%{id: id}, %Retex{activations: activations}) do 69 | Enum.any?(Map.get(activations, id, [])) 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /benchmark/example.exs: -------------------------------------------------------------------------------- 1 | defmodule Benchmark do 2 | defp isa(variable, type) do 3 | isa(variable: variable, type: type) 4 | end 5 | 6 | defp isa(fields) do 7 | Retex.Fact.Isa.new(fields) 8 | end 9 | 10 | defp has_attribute(owner, attribute, predicate, value) do 11 | has_attribute(owner: owner, attribute: attribute, predicate: predicate, value: value) 12 | end 13 | 14 | defp has_attribute(fields) do 15 | Retex.Fact.HasAttribute.new(fields) 16 | end 17 | 18 | defp create_rule(lhs: given, rhs: action) do 19 | %{ 20 | given: given, 21 | then: action 22 | } 23 | end 24 | 25 | defp rule(n, type \\ :Thing) do 26 | given = [ 27 | has_attribute(:Thing, :status, :==, "$a_#{n}"), 28 | has_attribute(:Thing, :premium, :==, true) 29 | ] 30 | 31 | action = [ 32 | {"$thing_#{n}", :account_status, "$a_#{n}"} 33 | ] 34 | 35 | rule = create_rule(lhs: given, rhs: action) 36 | end 37 | 38 | def run() do 39 | wme = Retex.Wme.new(:Account, :status, :silver) 40 | wme_2 = Retex.Wme.new(:Account, :premium, true) 41 | wme_3 = Retex.Wme.new(:Family, :size, 10) 42 | 43 | wme_4 = Retex.Wme.new(:Account, :status, :silver) 44 | wme_5 = Retex.Wme.new(:AccountC, :status, :silver) 45 | wme_5 = Retex.Wme.new(:AccountD, :status, :silver) 46 | number_of_rules = 100_000 47 | 48 | require Logger 49 | 50 | Logger.info("Adding #{number_of_rules} rules...") 51 | 52 | result = 53 | Timer.tc(fn -> 54 | Enum.reduce(1..number_of_rules, Retex.new(), fn n, network -> 55 | Retex.add_production(network, rule(n)) 56 | end) 57 | end) 58 | 59 | duration = result[:humanized_duration] 60 | rules_100_000 = result[:reply] 61 | Logger.info("Adding #{number_of_rules} rules took #{duration}") 62 | 63 | given_matching = [ 64 | has_attribute(:Account, :status, :==, "$account"), 65 | has_attribute(:Account, :premium, :==, true) 66 | ] 67 | 68 | action_2 = [ 69 | {"$thing", :account_status, "$account"} 70 | ] 71 | 72 | rule = create_rule(lhs: given_matching, rhs: action_2) 73 | network = Retex.add_production(rules_100_000, rule) 74 | 75 | Logger.info("Network info #{Graph.info(rules_100_000.graph) |> inspect()}") 76 | 77 | result = 78 | Timer.tc(fn -> 79 | network 80 | |> Retex.add_wme(wme) 81 | |> Retex.add_wme(wme_2) 82 | |> Retex.add_wme(wme_3) 83 | |> Retex.add_wme(wme_4) 84 | |> Retex.add_wme(wme_5) 85 | end) 86 | 87 | duration = result[:humanized_duration] 88 | network = result[:reply] 89 | 90 | Logger.info( 91 | "Adding 6 working memories took #{duration} and matched #{Enum.count(network.agenda)} rules" 92 | ) 93 | 94 | IO.inspect(agenda: network.agenda) 95 | end 96 | end 97 | 98 | Benchmark.run() 99 | -------------------------------------------------------------------------------- /lib/nodes/beta_memory.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex.Node.BetaMemory do 2 | @moduledoc """ 3 | A BetaMemory works like a two input node in Rete. It is simply a join node 4 | between two tests that have passed successfully. The activation of a BetaMemory 5 | happens if the two parents (left and right) have been activated and the bindings 6 | are matching for both of them. 7 | """ 8 | defstruct id: nil 9 | @type t :: %Retex.Node.BetaMemory{} 10 | 11 | def new(id) do 12 | %__MODULE__{id: id} 13 | end 14 | 15 | defimpl Retex.Protocol.Activation do 16 | alias Retex.Protocol.Activation 17 | 18 | def activate(neighbor, rete, wme, bindings, _tokens) do 19 | [left, right] = Graph.in_neighbors(rete.graph, neighbor) 20 | 21 | with true <- Activation.active?(left, rete), 22 | true <- Activation.active?(right, rete), 23 | left_tokens <- Map.get(rete.tokens, left.id), 24 | right_tokens <- Map.get(rete.tokens, right.id), 25 | new_tokens <- matching_tokens(neighbor, wme, right_tokens, left_tokens), 26 | true <- Enum.any?(new_tokens) do 27 | rete 28 | |> Retex.create_activation(neighbor, wme) 29 | |> Retex.add_token(neighbor, wme, bindings, new_tokens) 30 | |> Retex.continue_traversal(bindings, neighbor, wme) 31 | else 32 | _anything -> 33 | Retex.stop_traversal(rete, %{}) 34 | end 35 | end 36 | 37 | defp matching_tokens(_, _, left, nil), do: left 38 | defp matching_tokens(_, _, nil, right), do: right 39 | 40 | defp matching_tokens(node, wme, left, right) do 41 | for %{bindings: left_bindings} <- left, %{bindings: right_bindings} <- right do 42 | if variables_match?(left_bindings, right_bindings) do 43 | [ 44 | %{ 45 | Retex.Token.new() 46 | | wmem: wme, 47 | node: node.id, 48 | bindings: Map.merge(left_bindings, right_bindings) 49 | } 50 | ] 51 | else 52 | [] 53 | end 54 | end 55 | |> List.flatten() 56 | end 57 | 58 | defp variables_match?(left, right) do 59 | left_bindings_match_in_right_bindings?(left, right) && 60 | right_bindings_match_in_left_bindings?(right, left) 61 | end 62 | 63 | defp left_bindings_match_in_right_bindings?(left, right) do 64 | Enum.reduce_while(left, true, fn {key, left_value}, true -> 65 | if Map.get(right, key, left_value) == left_value, 66 | do: {:cont, true}, 67 | else: {:halt, false} 68 | end) 69 | end 70 | 71 | defp right_bindings_match_in_left_bindings?(right, left) do 72 | Enum.reduce_while(right, true, fn {key, right_value}, true -> 73 | if Map.get(left, key, right_value) == right_value, 74 | do: {:cont, true}, 75 | else: {:halt, false} 76 | end) 77 | end 78 | 79 | @spec active?(%{id: any}, Retex.t()) :: boolean() 80 | def active?(%{id: id}, %Retex{activations: activations}) do 81 | Enum.any?(Map.get(activations, id, [])) 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Retex.MixProject do 2 | use Mix.Project 3 | @source_url "https://github.com/lorenzosinisi/retex" 4 | @version "0.1.11" 5 | 6 | def project do 7 | [ 8 | app: :retex, 9 | version: @version, 10 | elixir: "~> 1.12", 11 | aliases: aliases(), 12 | start_permanent: Mix.env() == :prod, 13 | source_url: "https://github.com/lorenzosinisi/retex", 14 | description: description(), 15 | package: package(), 16 | deps: deps(), 17 | docs: docs(), 18 | test_coverage: [tool: ExCoveralls], 19 | preferred_cli_env: [ 20 | coveralls: :test, 21 | "coveralls.detail": :test, 22 | "coveralls.post": :test, 23 | "coveralls.html": :test 24 | ], 25 | dialyzer: [ 26 | plt_file: {:no_warn, "priv/plts/dialyzer.plt"}, 27 | ignore_warnings: ".dialyzer_ignore.exs" 28 | ] 29 | ] 30 | end 31 | 32 | defp description do 33 | """ 34 | Rete algorithm in Elixir 35 | """ 36 | end 37 | 38 | # Run "mix help compile.app" to learn about applications. 39 | def application do 40 | [ 41 | extra_applications: [:logger] 42 | ] 43 | end 44 | 45 | defp package do 46 | [ 47 | files: ["lib", "mix.exs", "README.md", "LICENSE"], 48 | maintainers: ["Lorenzo Sinisi"], 49 | licenses: ["Apache 2.0"], 50 | links: %{"GitHub" => "https://github.com/lorenzosinisi/retex"} 51 | ] 52 | end 53 | 54 | # Run "mix help deps" to learn about dependencies. 55 | defp deps do 56 | [ 57 | {:libgraph, "~> 0.16.0"}, 58 | {:uuid_tools, "~> 0.1.0"}, 59 | {:duration_tc, "~> 0.1.1", only: :dev}, 60 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 61 | {:dialyxir, "~> 1.3", only: [:dev], runtime: false}, 62 | {:excoveralls, "~> 0.10", only: :test}, 63 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, 64 | {:sobelow, "~> 0.7", only: [:dev, :test], runtime: false} 65 | ] 66 | end 67 | 68 | defp docs do 69 | [ 70 | extras: [ 71 | "CHANGELOG.md": [], 72 | LICENSE: [title: "License"], 73 | "README.md": [title: "Overview"] 74 | ], 75 | assets: "assets", 76 | main: "readme", 77 | canonical: "http://hexdocs.pm/retex", 78 | homepage_url: @source_url, 79 | source_url: @source_url, 80 | source_ref: "v#{@version}", 81 | before_closing_body_tag: &before_closing_body_tag/1 82 | ] 83 | end 84 | 85 | defp before_closing_body_tag(:html) do 86 | """ 87 | 88 | 106 | """ 107 | end 108 | 109 | defp before_closing_body_tag(_), do: "" 110 | 111 | defp aliases do 112 | [ 113 | test: [ 114 | "format", 115 | "coveralls", 116 | "credo", 117 | "sobelow --skip -i Config.HTTPS --verbose" 118 | ] 119 | ] 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, 3 | "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, 4 | "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, 5 | "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, 6 | "dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"}, 7 | "duration_tc": {:hex, :duration_tc, "0.1.1", "fa72b26fc8d97abb9492f2e50fc90306e1156125bd4f4c75aa60cd335e5658f1", [:mix], [{:timex, "~> 3.5", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "d8ec390dba34f5164e085fddb3642353fe4f024ceb51653db042a26a4519956e"}, 8 | "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, 9 | "earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"}, 10 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 11 | "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, 12 | "excoveralls": {:hex, :excoveralls, "0.16.1", "0bd42ed05c7d2f4d180331a20113ec537be509da31fed5c8f7047ce59ee5a7c5", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dae763468e2008cf7075a64cb1249c97cb4bc71e236c5c2b5e5cdf1cfa2bf138"}, 13 | "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"}, 14 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 15 | "gettext": {:hex, :gettext, "0.22.3", "c8273e78db4a0bb6fba7e9f0fd881112f349a3117f7f7c598fa18c66c888e524", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "935f23447713954a6866f1bb28c3a878c4c011e802bcd68a726f5e558e4b64bd"}, 16 | "graphvix": {:hex, :graphvix, "1.0.0", "e1c2e8f2950c0ff43e64f1d3788d095ed7481b29cccaf63f80b2fdf751c6cdd3", [:mix], [], "hexpm"}, 17 | "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, 18 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 19 | "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, 20 | "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, 21 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 22 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, 23 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, 24 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 25 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 26 | "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, 27 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 28 | "sanskrit": {:git, "https://github.com/lorenzosinisi/sanskrit", "a599df828a67daedd179cfa34518c254058941b0", []}, 29 | "sobelow": {:hex, :sobelow, "0.12.2", "45f4d500e09f95fdb5a7b94c2838d6b26625828751d9f1127174055a78542cf5", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2f0b617dce551db651145662b84c8da4f158e7abe049a76daaaae2282df01c5d"}, 30 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 31 | "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, 32 | "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, 33 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 34 | "uuid_tools": {:hex, :uuid_tools, "0.1.1", "e94e05551dcfd085a127c11112b0e6d24a98963659bf2f16615745c492e00e38", [:mix], [], "hexpm", "eafa199917316fece589c8768427c0bac6d6959a523482d130f1997a674c0da4"}, 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lib/retex.ex: -------------------------------------------------------------------------------- 1 | defmodule Retex do 2 | @moduledoc """ 3 | The algorithm utilizes symbols to create an internal representation of the world. 4 | Each element in the real world is converted into a triple known as a "Working Memory Element" (`Retex.Wme.t()`), 5 | represented as {Entity, attribute, attribute_value}. 6 | 7 | The world is represented through facts (WMEs) and Rules. 8 | A Rule consists of two essential parts: the "given" (right side) and the "then" (left side). 9 | 10 | To perform inference, the rule generates a directed graph starting from a common and generic Root node, 11 | which branches out to form leaf nodes. The branches from the Root node correspond to the initial part 12 | of the WME, representing the working memory elements or "Entity". For instance, if we want to 13 | represent a customer's account status as "silver", we would encode it as "{Customer, account_status, silver}". 14 | Alternatively, with the use of a struct, we can achieve the same representation as `Retex.Wme.new("Customer", "account status", "silver")` 15 | 16 | 17 | 18 | ## The struct 19 | This module also defines a Struct with the following fields: 20 | 21 | 1. `graph: Graph.new()`: This field is initialized with the value returned by the `Graph.new()` function. It is a reference to a directed graph data structure (using `libgraph`) 22 | that represents a network of interconnected nodes and vertices. 23 | 24 | 2. `wmes: %{}`: This field is initialized as an empty map. It is expected to store "working memory elements" (WMEs), which are pieces of information or facts used in the system. 25 | 26 | 3. `agenda: []`: This field is initialized as an empty list. It is intended to store a collection of tasks or items that need to be processed or executed. The agenda typically represents a prioritized queue of pending actions. 27 | 28 | 4. `activations: %{}`: This field is initialized as an empty map. It is used to store information related to the activations of nodes in the network. 29 | 30 | 5. `wme_activations: %{}`: This field is initialized as an empty map. It is similar to the `activations` field but specifically focuses on the activations of working memory elements (WMEs) and can serve as reverse lookup of which facts have activated which no. 31 | 32 | 6. `tokens: MapSet.new()`: This field is initialized with the value returned by the `MapSet.new()` function. Will be deprecated soon because it has no use but was a porting from the original paper. 33 | 34 | 7. `bindings: %{}`: This field is initialized as an empty map. It is used to store variable bindings or associations between variables and their corresponding values. This can be useful for tracking and manipulating data within the system. 35 | 36 | 8. `pending_activation: []`: This field is initialized as an empty list. It is likely used to keep track of activations that are pending or awaiting further processing. The exact meaning and usage of these pending activations would depend on the system's design. 37 | """ 38 | 39 | @type t() :: %Retex{} 40 | alias Retex.{Fact, Node, Protocol, Protocol.AlphaNetwork, Token} 41 | 42 | alias Node.{ 43 | BetaMemory, 44 | PNode, 45 | Select, 46 | Test, 47 | Type 48 | } 49 | 50 | @type action :: %{given: list(Retex.Wme.t()), then: list(Retex.Wme.t())} 51 | @type network_node :: Type.t() | Test.t() | Select.t() | PNode.t() | BetaMemory.t() 52 | 53 | # the Retex network is made of the following elements: 54 | defstruct graph: Graph.new(), 55 | wmes: %{}, 56 | agenda: [], 57 | activations: %{}, 58 | wme_activations: %{}, 59 | tokens: MapSet.new(), 60 | bindings: %{}, 61 | pending_activation: [] 62 | 63 | @doc """ 64 | Generate a new Retext.Root.t() struct that represents the first node of the network. 65 | An anonymous node that functions just as connector for the type nodes (`Retex.Node.Type.t()`) 66 | """ 67 | @spec root_vertex :: Retex.Root.t() 68 | def root_vertex, do: Retex.Root.new() 69 | 70 | @doc @moduledoc 71 | @spec new :: Retex.t() 72 | def new do 73 | %{graph: graph} = %Retex{} 74 | graph = Graph.add_vertex(graph, Retex.Root.new()) 75 | %Retex{graph: graph} 76 | end 77 | 78 | @doc """ 79 | Takes the network itself and a WME struct and tries to activate as many nodes as possible traversing the graph 80 | from the Root until each reachable branch executing a series of "tests" (pattern matching) at each node level. 81 | 82 | Each node is tested implementing the activation protocol, so to know if how the test for the node against the WME 83 | works check their protocol implementation. 84 | """ 85 | @spec add_wme(Retex.t(), Retex.Wme.t()) :: Retex.t() 86 | def add_wme(%Retex{} = network, %Retex.Wme{} = wme) do 87 | # just a timestamp might be useful for the future 88 | wme = Map.put(wme, :timestamp, :os.system_time(:seconds)) 89 | # store the new fact in memory with their ID 90 | network = %{network | wmes: Map.put(network.wmes, wme.id, wme)} 91 | # traverse the network for the Root node branching out and testing each node for possible 92 | # activations 93 | {network, bindings} = propagate_activations(network, root_vertex(), wme, network.bindings) 94 | 95 | # if there is some activation for variables store such mapping (variable name => value) in memory 96 | %{network | bindings: Map.merge(network.bindings, bindings)} 97 | end 98 | 99 | @doc """ 100 | A production is what is called a Rule in the original Rete paper from C. Forgy 101 | 102 | A production is a map of given and then and each of those fields contains a list of 103 | `Retex.Fact.t()` which can be tested against a `Retex.Wme.t()` 104 | """ 105 | @spec add_production(Retex.t(), %{given: list(Retex.Wme.t()), then: action()}) :: t() 106 | def add_production(%{graph: graph} = network, %{given: given, then: action}) do 107 | # Filters are extra tests that ca be added to the given part of the rule 108 | # it is an extra feature present only in Retex and a personal choice 109 | {filters, given} = Enum.split_with(given, &is_filter?/1) 110 | # Take each fact in the given part of the rule and find or create the corresponding node 111 | # this part of what you see in the graph at https://github.com/lorenzosinisi/retex#how-does-it-work 112 | {graph, alphas} = build_alpha_network(graph, given) 113 | # this second part is just the representation of each "and" in a rule. 114 | {beta_memory, graph} = build_beta_network(graph, alphas) 115 | 116 | # finally add the "then" part of the rule on the last leaf of that subgraph (generated by the Rule) 117 | graph = add_p_node(graph, beta_memory, action, filters) 118 | 119 | %{network | graph: graph} 120 | end 121 | 122 | defp is_filter?(%Fact.Filter{}), do: true 123 | defp is_filter?(_), do: false 124 | 125 | @doc """ 126 | Take each fact of the "given" part of the rule and construct the alpha part of the network destructuring the facts 127 | into "Root" -> "Entity" -> "Attribute" -> "Value" (if a node with the same value already exists it will be a noop) 128 | """ 129 | @spec build_alpha_network(Graph.t(), list()) :: {Graph.t(), list()} 130 | def build_alpha_network(graph, given) do 131 | Enum.reduce(given, {graph, []}, &AlphaNetwork.append(&1, &2)) 132 | end 133 | 134 | @doc """ 135 | After building the alpha network we will have a list of nodes which are the bottom of the new subnetwork, 136 | not connect those two by two. Take the firs two, join them with a new node, that that one node 137 | connect it with the next orphan node, keep doing it until all the facts of a rule are connected together and 138 | we have one last "Join" node. 139 | 140 | And example of a graph with only one "and/join" node: 141 | 142 | ```mermaid 143 | flowchart 144 | 2102090852["==100"] 145 | 2332826675["==silver"] 146 | 2833714732["[{:Discount, :code, 50}]"] 147 | 3108351631["Root"] 148 | 3726656564["Join"] 149 | 3801762854["miles"] 150 | 3860425667["Customer"] 151 | 3895425755["account_status"] 152 | 4112061991["Flight"] 153 | 2102090852 --> 3726656564 154 | 2332826675 --> 3726656564 155 | 3108351631 --> 3860425667 156 | 3108351631 --> 4112061991 157 | 3726656564 --> 2833714732 158 | 3801762854 --> 2102090852 159 | 3860425667 --> 3895425755 160 | 3895425755 --> 2332826675 161 | 4112061991 --> 3801762854 162 | ``` 163 | """ 164 | @spec build_beta_network(Graph.t(), list(network_node())) :: {list(network_node()), Graph.t()} 165 | def build_beta_network(graph, [first, second | list]) do 166 | id = Retex.hash(Enum.sort([first, second])) 167 | beta_memory = Node.BetaMemory.new(id) 168 | 169 | graph 170 | |> Graph.add_vertex(beta_memory) 171 | |> Graph.add_edge(first, beta_memory) 172 | |> Graph.add_edge(second, beta_memory) 173 | |> build_beta_network([beta_memory | list]) 174 | end 175 | 176 | def build_beta_network(graph, [beta_memory]) do 177 | {beta_memory, graph} 178 | end 179 | 180 | @doc """ 181 | The P node is the production node, just another name of a rule 182 | """ 183 | @spec add_p_node(Graph.t(), BetaMemory.t(), action(), list(Fact.Filter.t())) :: Graph.t() 184 | def add_p_node(graph, beta_memory, action, filters) do 185 | pnode = Node.PNode.new(action, filters) 186 | graph |> Graph.add_vertex(pnode) |> Graph.add_edge(beta_memory, pnode) 187 | end 188 | 189 | @max_phash 4_294_967_296 190 | @spec hash(any) :: String.t() 191 | def hash(:uuid4), do: UUIDTools.uuid4() 192 | 193 | def hash(data) do 194 | :erlang.phash2(data, @max_phash) 195 | end 196 | 197 | @spec replace_bindings(PNode.t(), map) :: PNode.t() 198 | def replace_bindings(%_{action: action_fun} = pnode, bindings) 199 | when is_function(action_fun) do 200 | %{pnode | action: action_fun, bindings: bindings} 201 | end 202 | 203 | def replace_bindings(%_{action: actions} = pnode, bindings) 204 | when is_list(actions) and is_map(bindings) do 205 | new_actions = Enum.map(actions, fn action -> replace_bindings(action, bindings) end) 206 | %{pnode | action: new_actions, bindings: bindings} 207 | end 208 | 209 | def replace_bindings(%Retex.Wme{} = action, bindings) when is_map(bindings) do 210 | populated = 211 | for {key, val} <- Map.from_struct(action), into: %{} do 212 | val = Map.get(bindings, val, val) 213 | {key, val} 214 | end 215 | 216 | struct(Retex.Wme, populated) 217 | end 218 | 219 | def replace_bindings(tuple, bindings) when is_map(bindings) and is_tuple(tuple) do 220 | List.to_tuple( 221 | for element <- Tuple.to_list(tuple) do 222 | if is_binary(element), do: Map.get(bindings, element, element), else: element 223 | end 224 | ) 225 | end 226 | 227 | def replace_bindings(anything, _bindings) do 228 | anything 229 | end 230 | 231 | @spec add_token(Retex.t(), network_node(), Retex.Wme.t(), map, list(Retex.Token.t())) :: 232 | Retex.t() 233 | def add_token( 234 | %Retex{tokens: rete_tokens} = rete, 235 | current_node, 236 | _wme, 237 | _bindings, 238 | [_ | _] = tokens 239 | ) do 240 | node_tokens = 241 | rete_tokens 242 | |> Map.get(current_node.id, []) 243 | |> MapSet.new() 244 | 245 | all_tokens = 246 | node_tokens 247 | |> MapSet.new() 248 | |> MapSet.union(MapSet.new(tokens)) 249 | 250 | new_tokens = Map.put(rete_tokens, current_node.id, all_tokens) 251 | 252 | %{rete | tokens: new_tokens} 253 | end 254 | 255 | def add_token(%Retex{tokens: rete_tokens} = rete, current_node, wme, bindings, tokens) do 256 | node_tokens = 257 | rete_tokens 258 | |> Map.get(current_node.id, []) 259 | |> MapSet.new() 260 | 261 | token = Token.new() 262 | 263 | token = %{ 264 | token 265 | | wmem: wme, 266 | node: current_node.id, 267 | bindings: bindings 268 | } 269 | 270 | all_tokens = 271 | [token] 272 | |> MapSet.new() 273 | |> MapSet.union(node_tokens) 274 | |> MapSet.union(MapSet.new(tokens)) 275 | 276 | new_tokens = Map.put(rete_tokens, current_node.id, all_tokens) 277 | %{rete | tokens: new_tokens} 278 | end 279 | 280 | @spec create_activation(Retex.t(), network_node(), Retex.Wme.t()) :: Retex.t() 281 | def create_activation( 282 | %__MODULE__{activations: activations, wme_activations: wme_activations} = rete, 283 | current_node, 284 | wme 285 | ) do 286 | node_activations = Map.get(activations, current_node.id, []) 287 | new_activations = [wme.id | node_activations] 288 | new_rete = %{rete | activations: Map.put(activations, current_node.id, new_activations)} 289 | previous_wme_activations = Map.get(wme_activations, wme.id, []) 290 | 291 | new_wme_activations = 292 | Map.put(wme_activations, wme.id, [current_node.id | previous_wme_activations]) 293 | 294 | %{new_rete | wme_activations: new_wme_activations} 295 | end 296 | 297 | @spec propagate_activations( 298 | Retex.t(), 299 | network_node(), 300 | Retex.Wme.t(), 301 | map, 302 | list(Retex.Token.t()) 303 | ) :: {Retex.t(), map} 304 | def propagate_activations( 305 | %Retex{graph: graph} = rete, 306 | %{} = current_node, 307 | %Retex.Wme{} = wme, 308 | bindings, 309 | new_tokens 310 | ) do 311 | graph 312 | |> Graph.out_neighbors(current_node) 313 | |> Enum.reduce({rete, bindings}, fn vertex, {network, bindings} -> 314 | propagate_activation(vertex, network, wme, bindings, new_tokens) 315 | end) 316 | end 317 | 318 | @spec propagate_activations(Retex.t(), network_node(), Retex.Wme.t(), map) :: {Retex.t(), map} 319 | def propagate_activations( 320 | %Retex{graph: graph} = rete, 321 | %{} = current_node, 322 | %Retex.Wme{} = wme, 323 | bindings 324 | ) do 325 | graph 326 | |> Graph.out_neighbors(current_node) 327 | |> Enum.reduce({rete, bindings}, fn vertex, {network, bindings} -> 328 | propagate_activation(vertex, network, wme, bindings) 329 | end) 330 | end 331 | 332 | defp propagate_activation(neighbor, rete, wme, bindings, tokens \\ []) do 333 | Protocol.Activation.activate(neighbor, rete, wme, bindings, tokens) 334 | end 335 | 336 | @spec deactivate_descendants(Retex.t(), network_node()) :: Retex.t() 337 | def deactivate_descendants(%Retex{activations: activations} = rete, %{} = current_node) do 338 | %{graph: graph} = rete 339 | children = Graph.out_neighbors(graph, current_node) 340 | 341 | Enum.reduce(children, rete, fn %type{} = vertex, network -> 342 | if type == Retex.Node.PNode do 343 | %{ 344 | network 345 | | agenda: Enum.reject(network.agenda, fn pnode -> pnode.id == vertex.id end) 346 | } 347 | else 348 | new_network = %{network | activations: Map.put(activations, vertex.id, [])} 349 | deactivate_descendants(new_network, vertex) 350 | end 351 | end) 352 | end 353 | 354 | @spec continue_traversal(Retex.t(), map, network_node(), Retex.Wme.t(), list(Retex.Token.t())) :: 355 | {Retex.t(), map} 356 | def continue_traversal( 357 | %Retex{} = new_rete, 358 | %{} = new_bindings, 359 | %_{} = current_node, 360 | %Retex.Wme{} = wme, 361 | tokens 362 | ) do 363 | propagate_activations(new_rete, current_node, wme, new_bindings, tokens) 364 | end 365 | 366 | @spec continue_traversal(Retex.t(), map, network_node(), Retex.Wme.t()) :: {Retex.t(), map} 367 | def continue_traversal( 368 | %Retex{} = new_rete, 369 | %{} = new_bindings, 370 | %_{} = current_node, 371 | %Retex.Wme{} = wme 372 | ) do 373 | propagate_activations(new_rete, current_node, wme, new_bindings) 374 | end 375 | 376 | @spec stop_traversal(Retex.t(), map) :: {Retex.t(), map} 377 | def stop_traversal(%Retex{} = rete, %{} = bindings) do 378 | {rete, bindings} 379 | end 380 | end 381 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Retex 2 | 3 | ![Elixir CI](https://github.com/lorenzosinisi/retex/workflows/Elixir%20CI/badge.svg) 4 | 5 | # The Rete Algorithm 6 | 7 | "The Rete Match Algorithm is an efficient method for comparing a large collection of patterns to a large collection of objects. It finds all the objects that match each pattern. The algorithm was developed for use in production system interpreters, and it has been used for systems containing from a few hundred to more than a thousand patterns and objects" - C. Forgy 8 | 9 | **Boilerplate/PoC of a version of the Rete Algorithm implementated in Elixir** 10 | 11 | Rete is a complex stateful algorithm, this is an attempt of reproducing it with some slight modifications, using a functional immutable language such as Elixir/Erlang. [Read more about Rete](http://www.csl.sri.com/users/mwfong/Technical/RETE%20Match%20Algorithm%20-%20Forgy%20OCR.pdf) 12 | 13 | ## Requirements 14 | 15 | - Erlang/OTP 24 16 | - Elixir 1.14.2 (compiled with Erlang/OTP 22) 17 | 18 | ## How does it work? 19 | 20 | The algorithm utilizes symbols to create an internal representation of the world. Each element in the real world is converted into a triple known as a "Working Memory Element" (`Retex.Wme.t()`), represented as {Entity, attribute, attribute_value}. 21 | 22 | The world is represented through facts (WMEs) and Rules. A Rule consists of two essential parts: the "given" (right side) and the "then" (left side). 23 | 24 | To perform inference, the rule generates a directed graph starting from a common and generic Root node, which branches out to form leaf nodes. The branches from the Root node correspond to the initial part of the WME, representing the working memory elements or "Entity". For instance, if we want to represent a customer's account status as "silver", we would encode it as "{Customer, account_status, silver}". Alternatively, with the use of a struct, we can achieve the same representation as Retex.Wme.new("Customer", "account status", "silver"). 25 | 26 | Now, let's explore how this would appear when compiling the rete algorithm with Retex: 27 | 28 | ```mermaid 29 | flowchart 30 | 2332826675[==silver] 31 | 3108351631[Root] 32 | 3860425667[Customer] 33 | 3895425755[account_status] 34 | 3108351631 --> 3860425667 35 | 3860425667 --> 3895425755 36 | 3895425755 --> 2332826675 37 | ``` 38 | 39 | **example nr. 1** 40 | 41 | Now, let's examine the graph, which consists of four nodes in the following order: 42 | 43 | 1. The Root node 44 | 1. This node serves as the root for all type nodes, such as Account, Customer, God, Table, and so on. 45 | 2. The Customer node 46 | 1. Also known as a Type node, it stores each known "type" of entity recognized by the algorithm. 47 | 3. The account_status node 48 | 1. Referred to as a Select node, it represents the attribute name of the entity being described. 49 | 4. the ==silver node 50 | 1. Known as a Test node, it includes the == symbol, indicating that the value of Customer.account_status is checked against "silver" as a literal string (tests can use all Elixir comparison symbols). 51 | 52 | By expanding this network, we can continue mapping various aspects of the real world using any desired triple. Let's consider the entity representing a Flight, specifically its number of miles. We can represent this as {Flight, miles, 100} to signify a flight with a mileage of 100. Now, let's incorporate this into our network and observe the resulting graph: 53 | 54 | Let's add this to our network and check what kind of graph we will get: 55 | 56 | ```mermaid 57 | flowchart 58 | 2102090852[==100] 59 | 2332826675[==silver] 60 | 3108351631[Root] 61 | 3801762854[miles] 62 | 3860425667[Customer] 63 | 3895425755[account_status] 64 | 4112061991[Flight] 65 | 3108351631 --> 3860425667 66 | 3108351631 --> 4112061991 67 | 3801762854 --> 2102090852 68 | 3860425667 --> 3895425755 69 | 3895425755 --> 2332826675 70 | 4112061991 --> 3801762854 71 | ``` 72 | 73 | **example nr. 2** 74 | 75 | Now we begin to observe the modeling of more complex scenarios. Let's consider the addition of our first inference to the network, which involves introducing our first rule. 76 | 77 | The rule we want to encode states that when the Customer's account_status is "silver" and the Flight's miles are exactly "100," we should apply a discount to the Customer entity. 78 | 79 | Let's examine how our network will appear after incorporating this rule: 80 | 81 | ```mermaid 82 | flowchart 83 | 2102090852["==100"] 84 | 2332826675["==silver"] 85 | 2833714732["[{:Discount, :code, 50}]"] 86 | 3108351631["Root"] 87 | 3726656564["Join"] 88 | 3801762854["miles"] 89 | 3860425667["Customer"] 90 | 3895425755["account_status"] 91 | 4112061991["Flight"] 92 | 2102090852 --> 3726656564 93 | 2332826675 --> 3726656564 94 | 3108351631 --> 3860425667 95 | 3108351631 --> 4112061991 96 | 3726656564 --> 2833714732 97 | 3801762854 --> 2102090852 98 | 3860425667 --> 3895425755 99 | 3895425755 --> 2332826675 100 | 4112061991 --> 3801762854 101 | ``` 102 | 103 | Now we have constructed our network, which possesses a symbolic representation of the world and describes the relationships between multiple entities and their values to trigger a rule. Notably, the last node in the graph is represented as {:Discount, :code, 50}. 104 | 105 | Let's examine how we can interpret this graph step by step: 106 | 107 | 1. At the first level, we encounter the Root node, which serves as a placeholder. 108 | 2. At the second level, we find the Flight and Customer nodes branching out from the Root node. It's important to note that they are at the same level. 109 | 3. Both the Flight and Customer nodes branch out only once since they each have only one attribute. 110 | 4. Each attribute node (==100 and ==silver) branches out once again to indicate that if we encounter the attribute Customer.account_status, we should verify that its value is indeed "silver." 111 | 5. The last two nodes (==100 and ==silver) both connect to a new anonymous node called the Join node. 112 | 6. The Join node branches out only once, leading to the right-hand side of the rule (also known as the production node). 113 | 114 | This structure of the graph allows us to represent and process complex relationships and conditions within our network. 115 | 116 | ## What are join nodes? 117 | 118 | Join nodes are also what is called "beta memory" in the original C. Forgy paper. To make it simple we can assert that they group together a set of conditions that need to be true in order for a rule to fire. In our last example, the rule is: 119 | 120 | ``` 121 | # pseudocode 122 | given: Flight.miles == 100 and Customer.account_status == "silver" 123 | then: Discount.code == 50 124 | ``` 125 | 126 | In the graph representation, the Join node corresponds to the "and" in the "given" part of the rule. Its purpose is to evaluate and combine the conditions associated with its two parent nodes. Notably, a Join node can only have and will always have exactly two parents (incoming edges), which is a crucial characteristic of its design. 127 | 128 | By utilizing Join nodes, the network is able to effectively represent complex conditions and evaluate them in order to trigger the corresponding rules. 129 | 130 | ## What are production nodes? 131 | 132 | Production nodes, as named in the Forgy paper, refer to the right-hand side of a rule (also known as the "given" part). These nodes are exclusively connected to one incoming Join node in the network. 133 | 134 | To clarify, the purpose of a production node is to represent the actions or outcomes specified by the rule. It captures the consequences that should occur when the conditions specified in the Join node's associated "given" part are met. This relationship ensures that the rule's right-hand side is only triggered when the conditions of the Join node are satisfied. 135 | 136 | ## How do we use all of that after we built the network? 137 | 138 | Once we have a graph like the following and we know how to read it let's imagine we want to use to make inference and so to understand if we can give out 139 | such discount code to our customer. 140 | 141 | ```mermaid 142 | flowchart 143 | 2102090852["==100"] 144 | 2332826675["==silver"] 145 | 2833714732["[{:Discount, :code, 50}]"] 146 | 3108351631["Root"] 147 | 3726656564["Join"] 148 | 3801762854["miles"] 149 | 3860425667["Customer"] 150 | 3895425755["account_status"] 151 | 4112061991["Flight"] 152 | 2102090852 --> 3726656564 153 | 2332826675 --> 3726656564 154 | 3108351631 --> 3860425667 155 | 3108351631 --> 4112061991 156 | 3726656564 --> 2833714732 157 | 3801762854 --> 2102090852 158 | 3860425667 --> 3895425755 159 | 3895425755 --> 2332826675 160 | 4112061991 --> 3801762854 161 | ``` 162 | 163 | This process is called adding WMEs (working memory elements) to the network. As you might have already guessed there is very little difference between a WME and a part of a rule. 164 | 165 | `Retex` exposes the function `Retex.add_wme(t(), Retex.Wme.t())` which takes the network itself and a WME struct and tries to activate as many nodes as possible traversing the graph from the Root until each reachable branch executing a series of "tests" at each node. Let's see step by step how it would work. 166 | 167 | Let's rewrite that same graph adding some names to the edges so we can reference them in the description: 168 | 169 | ```mermaid 170 | flowchart 171 | 2102090852["==100"] 172 | 2332826675["==silver"] 173 | 2833714732["[{:Discount, :code, 50}]"] 174 | 3108351631["Root"] 175 | 3726656564["Join"] 176 | 3801762854["miles"] 177 | 3860425667["Customer"] 178 | 3895425755["account_status"] 179 | 4112061991["Flight"] 180 | 2102090852 --a--> 3726656564 181 | 2332826675 --b--> 3726656564 182 | 3108351631 --c--> 3860425667 183 | 3108351631 --d--> 4112061991 184 | 3726656564 --e--> 2833714732 185 | 3801762854 --f--> 2102090852 186 | 3860425667 --g--> 3895425755 187 | 3895425755 --h--> 2332826675 188 | 4112061991 --i--> 3801762854 189 | ``` 190 | 191 | Let's see what happens when adding the following working memory element to the Retex algorithm `Retex.Wme.new(:Flight, :miles, 100` 192 | 193 | 1. Retex will receive the WME and start testing the network from the Root node which passes down anything as it doesn't test for anything 194 | 2. The `Root` branches out in `n` nodes (Flight and Customer) 195 | 1. the branch `d` will find a type node with value "Flight" and this is the first part of the WME so the test is passing 196 | 1. the next branch from `d` is `i` which connects Flight to `miles` and so we test that the second part of the triple is exactly `miles`: the test is passing again 197 | 1. the next branch from `i` is `f` which finds a test node `== 100` which is the case of our new WME and so the test is passing 198 | 1. next is `a` which connects to the `Join` node which needs to be tested: a test for a Join node asserts that all incoming connections are active (their test passed) and given that the branch `b` is not yet tested the traversal for now ends here and the Join remains only 50% activated 199 | 2. the second branch to test is `c` which connects to `Customer` and this is not matching `Flight` so we can't go any further 200 | 201 | After adding the WME `Retex.Wme.new(:Flight, :miles, 100)` the only active branches and nodes are d, i, f and a 202 | 203 | Our rule can't be activated because the parent node `Join` is not fully active yet and so it can't propagate the current WME to the production node (which is sad but fair) 204 | 205 | Let's see what happens when adding the following working memory element to the Retex algorithm `Retex.Wme.new(:Customer, :account_status, "silver")` 206 | 207 | 1. Retex will receive the WME and start testing the network from the Root node which passes down anything as it doesn't test for anything 208 | 2. The `Root` branches out in `n` nodes (Flight and Customer) 209 | 1. the branch `c` will find a type node with value "Customer" and this is the first part of the WME so the test is passing 210 | 1. the next branch from `c` is `g` which connects Customer to `account_status` and so we test that the second part of the triple is exactly `account_status`: the test is passing again 211 | 1. the next branch from `h` finds a test node `== "silver"` which is the case of our new WME and so the test is passing 212 | 1. next is `b` which connects to the `Join` node which needs to be tested: a test for a Join node asserts that all incoming connections are active (their test passed) and given that the branch `a` is also already active (we stored that in a map) we can continue the traversal 213 | 1. Now we find a production node which tells us that the Discount code can be applied 214 | 2. the second branch to test is `d` which doesn't match so we can stop the traversal 215 | 216 | After adding the WME `Retex.Wme.new(:Customer, :account_status, "silver")` all nodes are active and so the production node ends up in the agenda (just an elixir list to keep track of all production nodes which are activated) 217 | 218 | We have now done inference and found an applicable rule. All we need to do now is to add the new WME to the network to check if any other node can be activated in the same way. 219 | 220 | Now imagine adding more and more complex rules and following the same strategy to find activable production nodes. The conditions will all be joined by a `Join` (and) node 221 | and will point to a production. 222 | 223 | ## Concepts 224 | 225 | - Retex compiles the rules using a directed acyclic graph data structure 226 | - The activation of nodes is done using a [State Monad and Forward Chaining](https://www.researchgate.net/publication/303626297_Forward_Chaining_with_State_Monad). 227 | - A list of bindings is stored at each active node in order to generate complete matches from partial ones 228 | 229 | ## Installation 230 | 231 | ```elixir 232 | def deps do 233 | [ 234 | {:retex, git: "https://github.com/lorenzosinisi/retex"} 235 | ] 236 | end 237 | ``` 238 | 239 | ## Installation using the wrapper NeuralBridge 240 | 241 | If you want you can have a predefined generic DSL and the wrapper NeuralBridge so that you don't have to build the rest of the Expert System from zero 242 | 243 | ```elixir 244 | # or visit https://neuralbridge.fly.dev <- an online playground to create rules and add facts 245 | def deps do 246 | [ 247 | {:neural_bridge, git: "https://github.com/lorenzosinisi/neural_bridge"} 248 | ] 249 | end 250 | ``` 251 | 252 | ## Examples and usage with NeuralBridge (a wrapper around Retex) 253 | 254 | ### Generic inferred knowledge 255 | 256 | ```elixir 257 | alias NeuralBridge.{Engine, Rule} 258 | engine = Engine.new("test") 259 | 260 | rules = [ 261 | Rule.new( 262 | id: 1, 263 | given: """ 264 | Person's name is equal "bob" 265 | """, 266 | then: """ 267 | Person's age is 23 268 | """ 269 | ), 270 | Rule.new( 271 | id: 2, 272 | given: """ 273 | Person's name is equal $name 274 | Person's age is equal 23 275 | """, 276 | then: fn production -> 277 | require Logger 278 | bindings = Map.get(production, :bindings) 279 | Logger.info(inspect(bindings)) 280 | end 281 | ) 282 | ] 283 | 284 | engine = Engine.add_rules(engine, rules) 285 | engine = Engine.add_facts(engine, "Person's name is \"bob\"") 286 | rule = List.first(engine.rule_engine.agenda) 287 | engine = Engine.apply_rule(engine, rule) 288 | 289 | Enum.each(engine.rule_engine.agenda, fn pnode -> 290 | Engine.apply_rule(engine, pnode) 291 | end) 292 | end # will log %{"$name" => "bob"} 293 | 294 | ``` 295 | 296 | ### Medical diagnosis 297 | 298 | ```elixir 299 | alias NeuralBridge.{Engine, Rule} 300 | engine = Engine.new("doctor_AI") 301 | 302 | engine = 303 | Engine.add_rules(engine, [ 304 | Rule.new( 305 | id: 1, 306 | given: """ 307 | Patient's fever is greater 38.5 308 | Patient's name is equal $name 309 | Patient's generic_weakness is equal "Yes" 310 | """, 311 | then: """ 312 | Patient's diagnosis is "flu" 313 | """ 314 | ), 315 | Rule.new( 316 | id: 2, 317 | given: """ 318 | Patient's fever is lesser 38.5 319 | Patient's name is equal $name 320 | Patient's generic_weakness is equal "No" 321 | """, 322 | then: """ 323 | Patient's diagnosis is "all good" 324 | """ 325 | ) 326 | ]) 327 | 328 | engine = 329 | Engine.add_facts(engine, """ 330 | Patient's fever is 39 331 | Patient's name is "Aylon" 332 | Patient's generic_weakness is "Yes" 333 | """) 334 | 335 | ## contains Patient's diagnnosis 336 | [ 337 | %_{ 338 | action: [ 339 | %Retex.Wme{ 340 | identifier: "Patient", 341 | attribute: "diagnosis", 342 | value: "flu" 343 | } 344 | ], 345 | bindings: %{"$name" => "Aylon"} 346 | } 347 | ] = engine.rule_engine.agenda 348 | 349 | ``` 350 | 351 | ## Test 352 | 353 | - Run `mix test` 354 | 355 | ## Benchmark adding 20k rules and triggering one 356 | 357 | - Run `elixir benchmark/rule_chain.exs` 358 | 359 | ## Warnings 360 | 361 | - Use at your own risk 362 | - This is just a template for complexer implementations of the described algorithms 363 | 364 | ## Resources and documentation 365 | 366 | - [Rete](https://cis.temple.edu/~giorgio/cis587/readings/rete.html) 367 | - [Retex at ElixirConf EU](https://www.youtube.com/watch?v=pvi5hURNzbk&ab_channel=CodeSync) 368 | - Retex is available as playground here: https://neuralbridge.fly.dev 369 | 370 | References: 371 | 372 | ``` 373 | Rete: A Fast Algorithm for the Many Pattern/Many Object Pattern Match Problem* by 374 | Charles L. Forgy 375 | Department of Computer Science, Carnegie-Mellon University, 376 | Pittsburgh, PA 15213, U.S.A. 377 | ``` 378 | -------------------------------------------------------------------------------- /test/retex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RetexTest do 2 | use ExUnit.Case 3 | alias Retex.Facts 4 | import Facts 5 | doctest Retex 6 | 7 | defp create_rule(lhs: given, rhs: action) do 8 | %{ 9 | given: given, 10 | then: action 11 | } 12 | end 13 | 14 | test "create a very simple rule of one condition" do 15 | given = [ 16 | has_attribute(:Customer, :account_status, :==, "silver"), 17 | has_attribute(:Flight, :miles, :==, "100") 18 | ] 19 | 20 | action = [ 21 | {:Discount, :code, 50} 22 | ] 23 | 24 | rule = create_rule(lhs: given, rhs: action) 25 | 26 | network = Retex.add_production(Retex.new(), rule) 27 | 28 | assert {:ok, _graph} = Retex.Serializer.serialize(network.graph) 29 | end 30 | 31 | test "adds the same rules twice" do 32 | given = [ 33 | has_attribute(:Account, :status, :==, "silver"), 34 | has_attribute(:Account, :status, :!=, "outdated"), 35 | has_attribute(:Flight, :partner, :!=, true) 36 | ] 37 | 38 | action = [ 39 | {:concession, 50} 40 | ] 41 | 42 | rule = create_rule(lhs: given, rhs: action) 43 | 44 | given_b = [ 45 | has_attribute(:Account, :status, :==, "silver"), 46 | has_attribute(:Account, :status, :!=, "outdated"), 47 | has_attribute(:Family, :size, :>=, 12), 48 | has_attribute(:Flight, :partner, :!=, true) 49 | ] 50 | 51 | action_b = [ 52 | {:concession, 110} 53 | ] 54 | 55 | rule_b = create_rule(lhs: given_b, rhs: action_b) 56 | 57 | network = 58 | Retex.new() 59 | |> Retex.add_production(rule) 60 | |> Retex.add_production(rule) 61 | |> Retex.add_production(rule_b) 62 | |> Retex.add_production(rule_b) 63 | 64 | assert 22 == Graph.edges(network.graph) |> Enum.count() 65 | assert 18 == Graph.vertices(network.graph) |> Enum.count() 66 | end 67 | 68 | test "add a production with existing attributes" do 69 | given = [ 70 | has_attribute(:Account, :status, :==, "silver"), 71 | has_attribute(:Account, :status, :!=, "outdated"), 72 | has_attribute(:Flight, :partner, :!=, true) 73 | ] 74 | 75 | action = [ 76 | {:concession, 50} 77 | ] 78 | 79 | rule = create_rule(lhs: given, rhs: action) 80 | 81 | given_b = [ 82 | has_attribute(:Account, :status, :==, "silver"), 83 | has_attribute(:Account, :status, :!=, "outdated"), 84 | has_attribute(:Family, :size, :>=, 12), 85 | has_attribute(:Flight, :partner, :!=, true) 86 | ] 87 | 88 | action_b = [ 89 | {:concession, 110} 90 | ] 91 | 92 | rule_b = create_rule(lhs: given_b, rhs: action_b) 93 | 94 | network = 95 | Retex.new() 96 | |> Retex.add_production(rule) 97 | |> Retex.add_production(rule_b) 98 | 99 | assert 22 == Graph.edges(network.graph) |> Enum.count() 100 | assert 18 == Graph.vertices(network.graph) |> Enum.count() 101 | end 102 | 103 | test "can convert the graph to a flowchart (mermaid)" do 104 | given = [ 105 | has_attribute(:Account, :status, :==, "silver"), 106 | has_attribute(:Account, :status, :!=, "outdated"), 107 | has_attribute(:Flight, :partner, :!=, true) 108 | ] 109 | 110 | action = fn something -> something end 111 | 112 | rule = create_rule(lhs: given, rhs: action) 113 | 114 | given_b = [ 115 | has_attribute(:Account, :status, :==, "silver"), 116 | has_attribute(:Account, :status, :!=, "outdated"), 117 | has_attribute(:Family, :size, :>=, 12), 118 | has_attribute(:Flight, :partner, :!=, true) 119 | ] 120 | 121 | action_b = fn something -> something end 122 | 123 | rule_b = create_rule(lhs: given_b, rhs: action_b) 124 | 125 | assert {:ok, _} = 126 | Retex.new() 127 | |> Retex.add_production(rule) 128 | |> Retex.add_production(rule_b) 129 | |> Map.get(:graph) 130 | |> Retex.Serializer.serialize() 131 | end 132 | 133 | test "add a production where the then clause is a function" do 134 | given = [ 135 | has_attribute(:Account, :status, :==, "silver"), 136 | has_attribute(:Account, :status, :!=, "outdated"), 137 | has_attribute(:Flight, :partner, :!=, true) 138 | ] 139 | 140 | action = fn something -> something end 141 | 142 | rule = create_rule(lhs: given, rhs: action) 143 | 144 | given_b = [ 145 | has_attribute(:Account, :status, :==, "silver"), 146 | has_attribute(:Account, :status, :!=, "outdated"), 147 | has_attribute(:Family, :size, :>=, 12), 148 | has_attribute(:Flight, :partner, :!=, true) 149 | ] 150 | 151 | action_b = fn something -> something end 152 | 153 | rule_b = create_rule(lhs: given_b, rhs: action_b) 154 | 155 | network = 156 | Retex.new() 157 | |> Retex.add_production(rule) 158 | |> Retex.add_production(rule_b) 159 | 160 | assert 22 == Graph.edges(network.graph) |> Enum.count() 161 | assert 18 == Graph.vertices(network.graph) |> Enum.count() 162 | end 163 | 164 | test "add a production" do 165 | given = [ 166 | has_attribute(:Account, :status, :==, "silver"), 167 | has_attribute(:Account, :status, :!=, "outdated"), 168 | has_attribute(:Flight, :partner, :!=, true) 169 | ] 170 | 171 | action = [ 172 | {:concession, 50} 173 | ] 174 | 175 | rule = create_rule(lhs: given, rhs: action) 176 | 177 | network = 178 | Retex.new() 179 | |> Retex.add_production(rule) 180 | 181 | assert 12 == Graph.edges(network.graph) |> Enum.count() 182 | assert 11 == Graph.vertices(network.graph) |> Enum.count() 183 | end 184 | 185 | describe "add_wme/2" do 186 | test "apply inference with rules in which we use isa statements that doesnt match" do 187 | wme = Retex.Wme.new(:Account, :status, :silver) 188 | wme_2 = Retex.Wme.new(:Account, :premium, true) 189 | wme_3 = Retex.Wme.new(:Family, :size, 10) 190 | 191 | given = [ 192 | isa("$thing", :AccountB), 193 | has_attribute("$thing", :status, :==, "$a"), 194 | has_attribute("$thing", :premium, :==, true) 195 | ] 196 | 197 | action = [ 198 | {"$thing", :account_status, "$a"} 199 | ] 200 | 201 | rule = create_rule(lhs: given, rhs: action) 202 | 203 | network = 204 | Retex.new() 205 | |> Retex.add_production(rule) 206 | |> Retex.add_wme(wme) 207 | |> Retex.add_wme(wme_2) 208 | |> Retex.add_wme(wme_3) 209 | 210 | assert network.agenda == [] 211 | end 212 | 213 | test "an activation of a negative node will deactivate its descendants" do 214 | given = [ 215 | isa("$thing", :Thing), 216 | is_not("$entity", :Flight) 217 | ] 218 | 219 | action = [ 220 | Retex.Wme.new(:Account, :id, 1) 221 | ] 222 | 223 | rule = create_rule(lhs: given, rhs: action) 224 | 225 | wme = Retex.Wme.new(:Thing, :status, :silver) 226 | wme_1 = Retex.Wme.new(:Flight, :status, :silver) 227 | 228 | network = 229 | Retex.new() 230 | |> Retex.add_production(rule) 231 | |> Retex.add_wme(wme) 232 | |> Retex.add_wme(wme_1) 233 | 234 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 235 | 236 | assert [] == agenda 237 | end 238 | 239 | test "apply inference with a negated attribute, when the negative node is active" do 240 | given = [ 241 | isa("$thing", :Thing), 242 | not_existing_attribute(:Thing, :status) 243 | ] 244 | 245 | action = [ 246 | Retex.Wme.new(:Thing, :status, 1) 247 | ] 248 | 249 | rule = create_rule(lhs: given, rhs: action) 250 | 251 | wme = Retex.Wme.new(:Thing, :status, :gold) 252 | 253 | network = 254 | Retex.new() 255 | |> Retex.add_production(rule) 256 | |> Retex.add_wme(wme) 257 | 258 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 259 | 260 | assert [] == agenda 261 | 262 | wme_1 = Retex.Wme.new(:Thing, :id, 1) 263 | network = Retex.add_wme(network, wme_1) 264 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 265 | 266 | assert [] == agenda 267 | end 268 | 269 | test "apply inference with a negated attribute, when the negative node is not active" do 270 | given = [ 271 | isa("$thing", :Thing), 272 | not_existing_attribute(:Thing, :status) 273 | ] 274 | 275 | action = [ 276 | Retex.Wme.new(:Thing, :status, 1) 277 | ] 278 | 279 | rule = create_rule(lhs: given, rhs: action) 280 | 281 | wme = Retex.Wme.new(:Thing, :id, 1) 282 | 283 | network = 284 | Retex.new() 285 | |> Retex.add_production(rule) 286 | |> Retex.add_wme(wme) 287 | 288 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 289 | 290 | assert [[%Retex.Wme{attribute: :status, identifier: :Thing, value: 1}]] = agenda 291 | 292 | wme_1 = Retex.Wme.new(:Thing, :status, :silver) 293 | network = Retex.add_wme(network, wme_1) 294 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 295 | 296 | assert [] == agenda 297 | end 298 | 299 | test "apply inference with a negated condition, when the negative node is active" do 300 | given = [ 301 | isa("$thing", :Thing), 302 | is_not("$entity", :Flight) 303 | ] 304 | 305 | action = [ 306 | Retex.Wme.new(:Account, :id, 1) 307 | ] 308 | 309 | rule = create_rule(lhs: given, rhs: action) 310 | 311 | wme = Retex.Wme.new(:Flight, :status, :silver) 312 | wme_1 = Retex.Wme.new(:Thing, :status, :silver) 313 | 314 | network = 315 | Retex.new() 316 | |> Retex.add_production(rule) 317 | |> Retex.add_wme(wme) 318 | |> Retex.add_wme(wme_1) 319 | 320 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 321 | 322 | assert [] == agenda 323 | end 324 | 325 | test "apply inference with a negated condition" do 326 | given = [ 327 | isa("$thing", :Thing), 328 | is_not("$entity", :Flight) 329 | ] 330 | 331 | action = [ 332 | Retex.Wme.new(:Account, :id, 1) 333 | ] 334 | 335 | rule = create_rule(lhs: given, rhs: action) 336 | 337 | wme = Retex.Wme.new(:Thing, :status, :silver) 338 | 339 | network = 340 | Retex.new() 341 | |> Retex.add_production(rule) 342 | |> Retex.add_wme(wme) 343 | 344 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 345 | 346 | assert [ 347 | [ 348 | %Retex.Wme{ 349 | attribute: :id, 350 | timestamp: nil, 351 | identifier: :Account, 352 | value: 1 353 | } 354 | ] 355 | ] = agenda 356 | end 357 | 358 | test "apply inference and replace variables in a WME" do 359 | given = [ 360 | has_attribute(:Account, :status, :==, "$a"), 361 | has_attribute(:Account, :premium, :==, true) 362 | ] 363 | 364 | action = [ 365 | Retex.Wme.new("$thing", :account_status, "$a") 366 | ] 367 | 368 | rule = create_rule(lhs: given, rhs: action) 369 | 370 | given_2 = [ 371 | has_attribute(:AccountB, :status, :==, "$a_a"), 372 | has_attribute(:AccountB, :premium, :==, true) 373 | ] 374 | 375 | action_2 = [ 376 | {"$thing_a", :account_status, "$a_a"} 377 | ] 378 | 379 | rule_2 = create_rule(lhs: given_2, rhs: action_2) 380 | 381 | given_3 = [ 382 | has_attribute(:Account, :status, :==, "$a"), 383 | has_attribute(:Account, :premium, :==, true), 384 | has_attribute(:AccountB, :status, :==, "$a_a"), 385 | has_attribute(:AccountB, :premium, :==, true) 386 | ] 387 | 388 | action_3 = [ 389 | {"$thing_3", :account_status, "$a_3"} 390 | ] 391 | 392 | rule_3 = create_rule(lhs: given_3, rhs: action_3) 393 | 394 | wme = Retex.Wme.new(:Account, :status, :silver) 395 | wme_2 = Retex.Wme.new(:Account, :premium, true) 396 | wme_3 = Retex.Wme.new(:Family, :size, 10) 397 | 398 | network = 399 | Retex.new() 400 | |> Retex.add_production(rule) 401 | |> Retex.add_production(rule_2) 402 | |> Retex.add_production(rule_3) 403 | |> Retex.add_wme(wme) 404 | |> Retex.add_wme(wme_2) 405 | |> Retex.add_wme(wme_3) 406 | 407 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 408 | 409 | assert [ 410 | [ 411 | %Retex.Wme{ 412 | attribute: :account_status, 413 | timestamp: nil, 414 | identifier: "$thing", 415 | value: :silver 416 | } 417 | ] 418 | ] = agenda 419 | 420 | wme_4 = Retex.Wme.new(:AccountB, :premium, true) 421 | wme_5 = Retex.Wme.new(:AccountB, :status, true) 422 | 423 | network = 424 | network 425 | |> Retex.add_wme(wme_4) 426 | |> Retex.add_wme(wme_5) 427 | 428 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) |> Enum.count() 429 | 430 | assert 3 == agenda 431 | end 432 | 433 | test "return only the bindings of the fully activated rule" do 434 | given = [ 435 | has_attribute(:Account, :status, :==, "$a"), 436 | has_attribute(:Account, :premium, :==, "$b"), 437 | has_attribute(:Account, :age, :==, "$a"), 438 | has_attribute(:Account, :age, :>, 21) 439 | ] 440 | 441 | action = [ 442 | {:Account, :activated_1, "$a"} 443 | ] 444 | 445 | rule = create_rule(lhs: given, rhs: action) 446 | 447 | given_2 = [ 448 | has_attribute(:Account, :status, :==, "$a"), 449 | filter("$a", :!==, :blue), 450 | has_attribute(:Account, :premium, :==, "$b"), 451 | has_attribute(:Account, :age, :>, 11) 452 | ] 453 | 454 | action_2 = [ 455 | {:Account, :activated_2, "$a"} 456 | ] 457 | 458 | rule_2 = create_rule(lhs: given_2, rhs: action_2) 459 | 460 | wme = Retex.Wme.new(:Account, :status, :silver) 461 | wme_2 = Retex.Wme.new(:Account, :premium, true) 462 | wme_5 = Retex.Wme.new(:Account, :status, :blue) 463 | wme_3 = Retex.Wme.new(:Account, :age, 10) 464 | wme_4 = Retex.Wme.new(:Account, :status, :silver) 465 | 466 | network = 467 | Retex.new() 468 | |> Retex.add_production(rule) 469 | |> Retex.add_production(rule_2) 470 | |> Retex.add_wme(wme) 471 | |> Retex.add_wme(wme_2) 472 | |> Retex.add_wme(wme_3) 473 | |> Retex.add_wme(wme_4) 474 | |> Retex.add_wme(wme_5) 475 | 476 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 477 | 478 | assert agenda == [] 479 | 480 | triggering = Retex.Wme.new(:Account, :age, 20) 481 | 482 | network = Retex.add_wme(network, triggering) 483 | 484 | agenda = network.agenda |> Enum.map(&Map.take(&1, [:bindings])) 485 | 486 | assert agenda == [%{bindings: %{"$a" => :silver, "$b" => true}}] 487 | end 488 | 489 | test "apply inference with rules in which we use isa statements" do 490 | given = [ 491 | has_attribute(:Account, :status, :==, "$a"), 492 | has_attribute(:Account, :premium, :==, true) 493 | ] 494 | 495 | action = [ 496 | {"$thing", :account_status, "$a"} 497 | ] 498 | 499 | rule = create_rule(lhs: given, rhs: action) 500 | 501 | given_2 = [ 502 | has_attribute(:AccountB, :status, :==, "$a_a"), 503 | has_attribute(:AccountB, :premium, :==, true) 504 | ] 505 | 506 | action_2 = [ 507 | {"$thing_a", :account_status, "$a_a"} 508 | ] 509 | 510 | rule_2 = create_rule(lhs: given_2, rhs: action_2) 511 | 512 | wme = Retex.Wme.new(:Account, :status, :silver) 513 | wme_2 = Retex.Wme.new(:Account, :premium, true) 514 | wme_3 = Retex.Wme.new(:Family, :size, 10) 515 | 516 | network = 517 | Retex.new() 518 | |> Retex.add_production(rule) 519 | |> Retex.add_production(rule_2) 520 | |> Retex.add_wme(wme) 521 | |> Retex.add_wme(wme_2) 522 | |> Retex.add_wme(wme_3) 523 | 524 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 525 | 526 | assert agenda == [[{"$thing", :account_status, :silver}]] 527 | end 528 | 529 | test "the bindings are returned upon node activation" do 530 | wme = Retex.Wme.new(:Account, :status, :silver) 531 | wme_2 = Retex.Wme.new(:Account, :premium, true) 532 | wme_3 = Retex.Wme.new(:Family, :size, 10) 533 | 534 | given = [ 535 | has_attribute(:Account, :status, :==, "$a"), 536 | has_attribute(:Account, :premium, :==, true) 537 | ] 538 | 539 | action = [ 540 | {"$thing", :account_status, "$a"} 541 | ] 542 | 543 | rule = create_rule(lhs: given, rhs: action) 544 | 545 | network = 546 | Retex.new() 547 | |> Retex.add_production(rule) 548 | |> Retex.add_wme(wme) 549 | |> Retex.add_wme(wme_2) 550 | |> Retex.add_wme(wme_3) 551 | 552 | agenda = network.agenda 553 | [pnode] = agenda 554 | assert pnode.bindings == %{"$a" => :silver} 555 | end 556 | 557 | test "apply inference with rules in which all elements are variables" do 558 | wme = Retex.Wme.new(:Account, :status, :silver) 559 | wme_2 = Retex.Wme.new(:Account, :premium, true) 560 | wme_3 = Retex.Wme.new(:Family, :size, 10) 561 | 562 | given = [ 563 | has_attribute(:Account, :status, :==, "$a"), 564 | has_attribute(:Account, :premium, :==, true) 565 | ] 566 | 567 | action = [ 568 | {"$thing", :account_status, "$a"} 569 | ] 570 | 571 | rule = create_rule(lhs: given, rhs: action) 572 | 573 | network = 574 | Retex.new() 575 | |> Retex.add_production(rule) 576 | |> Retex.add_wme(wme) 577 | |> Retex.add_wme(wme_2) 578 | |> Retex.add_wme(wme_3) 579 | 580 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 581 | 582 | [pnodes] = agenda 583 | 584 | assert pnodes == [{"$thing", :account_status, :silver}] 585 | end 586 | 587 | test "apply inference with the use of variables as types" do 588 | wme = Retex.Wme.new(:Account, :status, :silver) 589 | wme_2 = Retex.Wme.new(:Account, :premium, false) 590 | wme_3 = Retex.Wme.new(:Family, :size, 10) 591 | 592 | given = [ 593 | has_attribute(:Account, :status, :==, "$a"), 594 | has_attribute(:Account, :premium, :==, false), 595 | has_attribute(:Family, :size, :==, "$c") 596 | ] 597 | 598 | action = [ 599 | {"$thing", :account_status, "$a"} 600 | ] 601 | 602 | rule = create_rule(lhs: given, rhs: action) 603 | 604 | network = 605 | Retex.new() 606 | |> Retex.add_production(rule) 607 | |> Retex.add_wme(wme) 608 | |> Retex.add_wme(wme_2) 609 | |> Retex.add_wme(wme_3) 610 | 611 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 612 | assert agenda == [[{"$thing", :account_status, :silver}]] 613 | end 614 | 615 | test "apply inference with the use of variables and they DONT match" do 616 | wme = Retex.Wme.new(:Account, :status, :silver) 617 | wme_2 = Retex.Wme.new(:Family, :size, 10) 618 | 619 | given = [ 620 | has_attribute(:Account, :status, :==, "$a"), 621 | has_attribute(:Family, :size, :==, "$a") 622 | ] 623 | 624 | action = [ 625 | {:Flight, :account_status, "$a"} 626 | ] 627 | 628 | rule = create_rule(lhs: given, rhs: action) 629 | 630 | network = 631 | Retex.new() 632 | |> Retex.add_production(rule) 633 | |> Retex.add_wme(wme) 634 | |> Retex.add_wme(wme_2) 635 | 636 | agenda = network.agenda 637 | 638 | assert agenda == [] 639 | end 640 | 641 | test "apply inference with the use of variables and they match" do 642 | wme = Retex.Wme.new(:Account, :status, :silver) 643 | wme_2 = Retex.Wme.new(:Family, :size, :silver) 644 | 645 | given = [ 646 | has_attribute(:Account, :status, :==, "$a"), 647 | has_attribute(:Family, :size, :==, "$a") 648 | ] 649 | 650 | action = [ 651 | {:Flight, :account_status, "$a"} 652 | ] 653 | 654 | rule = create_rule(lhs: given, rhs: action) 655 | 656 | network = 657 | Retex.new() 658 | |> Retex.add_production(rule) 659 | |> Retex.add_wme(wme) 660 | |> Retex.add_wme(wme_2) 661 | 662 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 663 | 664 | assert ^agenda = [[{:Flight, :account_status, :silver}]] 665 | end 666 | 667 | test "tokens are created" do 668 | wme = Retex.Wme.new(:Account, :status, :silver) 669 | wme_2 = Retex.Wme.new(:Family, :size, :silver) 670 | 671 | given = [ 672 | has_attribute(:Account, :status, :==, "$a"), 673 | has_attribute(:Family, :size, :==, "$a") 674 | ] 675 | 676 | action = [ 677 | {:Flight, :account_status, "$a"} 678 | ] 679 | 680 | rule = create_rule(lhs: given, rhs: action) 681 | 682 | network = 683 | Retex.new() 684 | |> Retex.add_production(rule) 685 | |> Retex.add_wme(wme) 686 | |> Retex.add_wme(wme_2) 687 | 688 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 689 | 690 | assert agenda == [ 691 | [{:Flight, :account_status, :silver}] 692 | ] 693 | 694 | assert network.tokens !== nil 695 | end 696 | 697 | test "apply inference with the use of variables with two rules sharing var names and attribute names" do 698 | wme = Retex.Wme.new(:Account, :status, :silver) 699 | wme_2 = Retex.Wme.new(:Family, :size, 10) 700 | wme_4 = Retex.Wme.new(:Account, :status, 10) 701 | 702 | given = [ 703 | has_attribute(:Account, :status, :==, "$a"), 704 | has_attribute(:Family, :size, :==, 10) 705 | ] 706 | 707 | action = [ 708 | {:Flight, :account_status, "$a"} 709 | ] 710 | 711 | rule = create_rule(lhs: given, rhs: action) 712 | 713 | given_2 = [ 714 | has_attribute(:Account, :status, :==, "$a"), 715 | has_attribute(:Family, :size, :==, "$a") 716 | ] 717 | 718 | action_2 = [ 719 | {:Flight, :account_status_a, "$a"} 720 | ] 721 | 722 | rule_2 = create_rule(lhs: given_2, rhs: action_2) 723 | 724 | network = 725 | Retex.new() 726 | |> Retex.add_production(rule) 727 | |> Retex.add_production(rule_2) 728 | |> Retex.add_wme(wme) 729 | |> Retex.add_wme(wme_2) 730 | 731 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 732 | 733 | assert agenda == [[{:Flight, :account_status, :silver}]] 734 | 735 | network = 736 | network 737 | |> Retex.add_wme(wme_4) 738 | 739 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) |> Enum.sort() 740 | 741 | assert agenda == [ 742 | [{:Flight, :account_status, :silver}], 743 | [{:Flight, :account_status_a, 10}] 744 | ] 745 | end 746 | 747 | test "apply inference with the use of variables with two rules sharing var names" do 748 | wme = Retex.Wme.new(:Account, :status, :silver) 749 | wme_2 = Retex.Wme.new(:Family, :size, 10) 750 | wme_3 = Retex.Wme.new(:Family, :size_a, 10) 751 | wme_4 = Retex.Wme.new(:Account, :status_a, 10) 752 | 753 | given = [ 754 | has_attribute(:Account, :status, :==, "$a"), 755 | has_attribute(:Family, :size, :==, 10), 756 | has_attribute(:Account, :status, :!=, 10) 757 | ] 758 | 759 | action = [ 760 | {:Flight, :account_status, "$a"} 761 | ] 762 | 763 | rule = create_rule(lhs: given, rhs: action) 764 | 765 | given_2 = [ 766 | has_attribute(:Account, :status_a, :==, "$a"), 767 | has_attribute(:Family, :size_a, :==, "$a") 768 | ] 769 | 770 | action_2 = [ 771 | {:Flight, :account_status_a, "$a"} 772 | ] 773 | 774 | rule_2 = create_rule(lhs: given_2, rhs: action_2) 775 | 776 | network = 777 | Retex.new() 778 | |> Retex.add_production(rule) 779 | |> Retex.add_production(rule_2) 780 | |> Retex.add_wme(wme) 781 | |> Retex.add_wme(wme_2) 782 | 783 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 784 | 785 | assert agenda == [[{:Flight, :account_status, :silver}]] 786 | 787 | network = 788 | network 789 | |> Retex.add_wme(wme_3) 790 | |> Retex.add_wme(wme_4) 791 | 792 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 793 | 794 | assert agenda == [ 795 | [{:Flight, :account_status_a, 10}], 796 | [{:Flight, :account_status, :silver}] 797 | ] 798 | end 799 | 800 | test "apply inference with the use of variables" do 801 | wme = Retex.Wme.new(:Account, :status, :silver) 802 | wme_2 = Retex.Wme.new(:Family, :size, 10) 803 | 804 | given = [ 805 | has_attribute(:Account, :status, :==, "$a"), 806 | has_attribute(:Family, :size, :==, 10) 807 | ] 808 | 809 | action = [ 810 | {:Flight, :account_status, "$a"} 811 | ] 812 | 813 | rule = create_rule(lhs: given, rhs: action) 814 | 815 | network = 816 | Retex.new() 817 | |> Retex.add_production(rule) 818 | |> Retex.add_wme(wme) 819 | |> Retex.add_wme(wme_2) 820 | 821 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 822 | assert agenda == [[{:Flight, :account_status, :silver}]] 823 | end 824 | 825 | test "add a new wme, trigger production" do 826 | wme = Retex.Wme.new(:Account, :status, :silver) 827 | 828 | given = [ 829 | has_attribute(:Account, :status, :==, :silver) 830 | ] 831 | 832 | action = [ 833 | {:Flight, :account_status, "silver"} 834 | ] 835 | 836 | rule = create_rule(lhs: given, rhs: action) 837 | 838 | network = 839 | Retex.new() 840 | |> Retex.add_production(rule) 841 | |> Retex.add_wme(wme) 842 | 843 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) 844 | 845 | assert agenda == [[{:Flight, :account_status, "silver"}]] 846 | end 847 | 848 | test "add a new wme" do 849 | wme = Retex.Wme.new(:Account, :status, "silver") 850 | 851 | network = 852 | Retex.new() 853 | |> Retex.add_wme(wme) 854 | 855 | assert network.wmes |> Enum.count() == 1 856 | end 857 | end 858 | end 859 | --------------------------------------------------------------------------------