├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── config └── config.exs ├── examples └── example.livemd ├── lib ├── neural_bridge.ex └── neural_bridge │ ├── application.ex │ ├── function.ex │ ├── function │ ├── defined_functions.ex │ └── function_behaviour.ex │ ├── rule.ex │ ├── sanskrit_interpreter.ex │ └── session.ex ├── mix.exs ├── mix.lock └── test ├── engine_test.exs ├── integration_test.exs ├── neural_bridge_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.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 | neural_bridge-*.tar 24 | 25 | # Ignore .elixir_ls files 26 | /.elixir_ls 27 | 28 | -------------------------------------------------------------------------------- /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 2023 Lorenzo Sinisi 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NeuralBridge 2 | 3 | Online implementation demo and playground: https://neuralbridge.fly.dev 4 | 5 | https://www.loom.com/share/a68821dfdbac4e54b3cfd885cbe4a535?t=619 6 | 7 | **A way to mimic human decision making, incredibily fast - An expert system built in Elixir** 8 | 9 | "Soon after the dawn of modern computers in the late 1940s – early 1950s, researchers started realizing the immense potential these machines had for modern society. One of the first challenges was to make such machine capable of “thinking” like humans. In particular, making these machines capable of making important decisions the way humans do. The medical / healthcare field presented the tantalizing challenge to enable these machines to make medical diagnostic decisions. 10 | 11 | Thus, in the late 1950s, right after the information age had fully arrived, researchers started experimenting with the prospect of using computer technology to emulate human decision-making. For example, biomedical researchers started creating computer-aided systems for diagnostic applications in medicine and biology. These early diagnostic systems used patients’ symptoms and laboratory test results as inputs to generate a diagnostic outcome. These systems were often described as the early forms of expert systems." - [Wikipedia](https://en.wikipedia.org/wiki/Expert_system) 12 | 13 | ## How does it work? 14 | 15 | https://www.loom.com/share/f80bb9ca10e64af1b73456e9c7980bf8 16 | 17 | 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}. 18 | 19 | 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). 20 | 21 | 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"). 22 | 23 | Now, let's explore how this would appear when compiling the rete algorithm with Retex: 24 | 25 | ```mermaid 26 | flowchart 27 | 2332826675[==silver] 28 | 3108351631[Root] 29 | 3860425667[Customer] 30 | 3895425755[account_status] 31 | 3108351631 --> 3860425667 32 | 3860425667 --> 3895425755 33 | 3895425755 --> 2332826675 34 | ``` 35 | 36 | **example nr. 1** 37 | 38 | Now, let's examine the graph, which consists of four nodes in the following order: 39 | 40 | 1. The Root node 41 | 1. This node serves as the root for all type nodes, such as Account, Customer, God, Table, and so on. 42 | 2. The Customer node 43 | 1. Also known as a Type node, it stores each known "type" of entity recognized by the algorithm. 44 | 3. The account_status node 45 | 1. Referred to as a Select node, it represents the attribute name of the entity being described. 46 | 4. the ==silver node 47 | 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). 48 | 49 | 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: 50 | 51 | Let's add this to our network and check what kind of graph we will get: 52 | 53 | ```mermaid 54 | flowchart 55 | 2102090852[==100] 56 | 2332826675[==silver] 57 | 3108351631[Root] 58 | 3801762854[miles] 59 | 3860425667[Customer] 60 | 3895425755[account_status] 61 | 4112061991[Flight] 62 | 3108351631 --> 3860425667 63 | 3108351631 --> 4112061991 64 | 3801762854 --> 2102090852 65 | 3860425667 --> 3895425755 66 | 3895425755 --> 2332826675 67 | 4112061991 --> 3801762854 68 | ``` 69 | 70 | **example nr. 2** 71 | 72 | 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. 73 | 74 | 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. 75 | 76 | Let's examine how our network will appear after incorporating this rule: 77 | 78 | ```mermaid 79 | flowchart 80 | 2102090852["==100"] 81 | 2332826675["==silver"] 82 | 2833714732["[{:Discount, :code, 50}]"] 83 | 3108351631["Root"] 84 | 3726656564["Join"] 85 | 3801762854["miles"] 86 | 3860425667["Customer"] 87 | 3895425755["account_status"] 88 | 4112061991["Flight"] 89 | 2102090852 --> 3726656564 90 | 2332826675 --> 3726656564 91 | 3108351631 --> 3860425667 92 | 3108351631 --> 4112061991 93 | 3726656564 --> 2833714732 94 | 3801762854 --> 2102090852 95 | 3860425667 --> 3895425755 96 | 3895425755 --> 2332826675 97 | 4112061991 --> 3801762854 98 | ``` 99 | 100 | 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}. 101 | 102 | Let's examine how we can interpret this graph step by step: 103 | 104 | 1. At the first level, we encounter the Root node, which serves as a placeholder. 105 | 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. 106 | 3. Both the Flight and Customer nodes branch out only once since they each have only one attribute. 107 | 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." 108 | 5. The last two nodes (==100 and ==silver) both connect to a new anonymous node called the Join node. 109 | 6. The Join node branches out only once, leading to the right-hand side of the rule (also known as the production node). 110 | 111 | This structure of the graph allows us to represent and process complex relationships and conditions within our network. 112 | 113 | ## What are join nodes? 114 | 115 | 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: 116 | 117 | ``` 118 | # pseudocode 119 | given: Flight.miles == 100 and Customer.account_status == "silver" 120 | then: Discount.code == 50 121 | ``` 122 | 123 | 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. 124 | 125 | By utilizing Join nodes, the network is able to effectively represent complex conditions and evaluate them in order to trigger the corresponding rules. 126 | 127 | ## What are production nodes? 128 | 129 | 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. 130 | 131 | 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. 132 | 133 | ## How do we use all of that after we built the network? 134 | 135 | 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 136 | such discount code to our customer. 137 | 138 | ```mermaid 139 | flowchart 140 | 2102090852["==100"] 141 | 2332826675["==silver"] 142 | 2833714732["[{:Discount, :code, 50}]"] 143 | 3108351631["Root"] 144 | 3726656564["Join"] 145 | 3801762854["miles"] 146 | 3860425667["Customer"] 147 | 3895425755["account_status"] 148 | 4112061991["Flight"] 149 | 2102090852 --> 3726656564 150 | 2332826675 --> 3726656564 151 | 3108351631 --> 3860425667 152 | 3108351631 --> 4112061991 153 | 3726656564 --> 2833714732 154 | 3801762854 --> 2102090852 155 | 3860425667 --> 3895425755 156 | 3895425755 --> 2332826675 157 | 4112061991 --> 3801762854 158 | ``` 159 | 160 | 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. 161 | 162 | `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. 163 | 164 | Let's rewrite that same graph adding some names to the edges so we can reference them in the description: 165 | 166 | ```mermaid 167 | flowchart 168 | 2102090852["==100"] 169 | 2332826675["==silver"] 170 | 2833714732["[{:Discount, :code, 50}]"] 171 | 3108351631["Root"] 172 | 3726656564["Join"] 173 | 3801762854["miles"] 174 | 3860425667["Customer"] 175 | 3895425755["account_status"] 176 | 4112061991["Flight"] 177 | 2102090852 --a--> 3726656564 178 | 2332826675 --b--> 3726656564 179 | 3108351631 --c--> 3860425667 180 | 3108351631 --d--> 4112061991 181 | 3726656564 --e--> 2833714732 182 | 3801762854 --f--> 2102090852 183 | 3860425667 --g--> 3895425755 184 | 3895425755 --h--> 2332826675 185 | 4112061991 --i--> 3801762854 186 | ``` 187 | 188 | Let's see what happens when adding the following working memory element to the Retex algorithm `Retex.Wme.new(:Flight, :miles, 100` 189 | 190 | 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 191 | 2. The `Root` branches out in `n` nodes (Flight and Customer) 192 | 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 193 | 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 194 | 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 195 | 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 196 | 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 197 | 198 | After adding the WME `Retex.Wme.new(:Flight, :miles, 100)` the only active branches and nodes are d, i, f and a 199 | 200 | 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) 201 | 202 | Let's see what happens when adding the following working memory element to the Retex algorithm `Retex.Wme.new(:Customer, :account_status, "silver")` 203 | 204 | 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 205 | 2. The `Root` branches out in `n` nodes (Flight and Customer) 206 | 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 207 | 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 208 | 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 209 | 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 210 | 1. Now we find a production node which tells us that the Discount code can be applied 211 | 2. the second branch to test is `d` which doesn't match so we can stop the traversal 212 | 213 | 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) 214 | 215 | 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. 216 | 217 | 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 218 | and will point to a production. 219 | 220 | ## How does the rule application work? 221 | 222 | Each time you insert a fact (be it by inference from a rule or a new standalone fact), the agenda might grow or shrink and produce new applicable rules. Each rule will then be applied until each of them has been applied once for each matching fact. 223 | 224 | ## What kind of syntax can I use in the given of a rule? 225 | 226 | In the "given" of a rule you can only use comparison statements such as: 227 | 228 | ``` 229 | Lorenzo's surname is equal "sinisi" 230 | Lorenzo's age is greater 28 231 | Lorenzo's country is equal "Germany" 232 | Lorenzo's language is equal "italian" 233 | Germany's language is equal "german" 234 | Dog's age is unknown 235 | ``` 236 | 237 | And in a "then" part of a rule you can use insertion statements that translate to WMEs and functions: 238 | 239 | ``` 240 | Lorenzo's surname is "sinisi" 241 | Lorenzo's age is 28 242 | Lorenzo's country is "Germany" 243 | Lorenzo's language is "italian" 244 | Germany's language is "german" 245 | let $second_surname = second_surname_of($sinisi) 246 | Lorenzo's second_surname is $second_surname 247 | ``` 248 | 249 | ## Installation 250 | 251 | ```elixir 252 | def deps do 253 | [ 254 | {:neural_bridge, git: "https://github.com/lorenzosinisi/neural_bridge"} 255 | ] 256 | end 257 | ``` 258 | 259 | ## Configuration 260 | 261 | If you want custom defined functions for your "let" statements in your rules you can define a module taking `NeuralBridge.DefinedFunctions` as example. 262 | 263 | ```elixir 264 | config :neural_bridge, defined_functions: NeuralBridge.DefinedFunctions 265 | ``` 266 | 267 | There is no limit at what you can do with custom defined functions. Say you want to call an API endpoint when a rule is applied you could do the following: 268 | 269 | ```sanskrit 270 | # in the "then" of a rule use something like: 271 | let $api_response = call_api($some_user_id) 272 | ``` 273 | 274 | And then define and config a custom module like: 275 | 276 | ```elixir 277 | defmodule NeuralBridge.MyDefinedFunctions do 278 | @behaviour NeuralBridge.FunctionBehaviour 279 | 280 | @impl true 281 | def call("fake_user_api", [user_id]) do 282 | response = Req.get("www.users.com", %{id: user_id}) 283 | case response.status_code do 284 | 200 -> 285 | response.body 286 | _ -> 287 | raise "API request failed with status code #{response.status_code}" 288 | end 289 | end 290 | 291 | @impl true 292 | def call(name, args) do 293 | raise "Undefined function #{name} with args #{inspect(args)}" 294 | end 295 | end 296 | ``` 297 | 298 | ## Examples and usage 299 | 300 | # Rule engine in Elixir - Rete 301 | 302 | ```elixir 303 | Mix.install([ 304 | {:neural_bridge, git: "https://github.com/lorenzosinisi/neural_bridge"} 305 | ]) 306 | ``` 307 | 308 | ## Example 1: calculate the net salary of an employee in the UK 309 | 310 | ```elixir 311 | rules = [ 312 | NeuralBridge.Rule.new( 313 | id: 1, 314 | given: """ 315 | Person's salary is equal $salary 316 | """, 317 | then: """ 318 | let $monthly_salary = div($salary, 12) 319 | Person's monthly_salary is $monthly_salary 320 | """ 321 | ), 322 | NeuralBridge.Rule.new( 323 | id: 2, 324 | given: """ 325 | Person's monthly_salary is equal $monthly_salary 326 | """, 327 | then: """ 328 | let $payout = mult($monthly_salary, 0.64) 329 | Salary's net_amount is $payout 330 | """ 331 | ), 332 | NeuralBridge.Rule.new( 333 | id: 2, 334 | given: """ 335 | Salary's net_amount is equal $amount 336 | """, 337 | then: """ 338 | Salary's net_amount is $amount 339 | """ 340 | ) 341 | ] 342 | 343 | facts = """ 344 | Person's salary is 60000 345 | Person's employment_type is "Full-time" 346 | Person's location is "UK" 347 | """ 348 | 349 | NeuralBridge.Session.new("uk") 350 | |> NeuralBridge.Session.add_rules(rules) 351 | |> NeuralBridge.Session.add_facts(facts) 352 | |> Map.fetch!(:inferred_facts) 353 | ``` 354 | 355 | ## Example 2: dynamic pricing 356 | 357 | In this example, the rules calculate the discount to be applied to a customer based on the number of items they have bought in a month. The discount percentages are determined as follows: 358 | 359 | - If the customer has bought 5 items, the discount percentage is set to 20%. 360 | - If the customer has bought less than 2 items, the discount percentage is set to 0%. 361 | - If the customer has bought exactly 3 items, the discount percentage is set to 10%. 362 | 363 | ```elixir 364 | rules = [ 365 | NeuralBridge.Rule.new( 366 | id: 1, 367 | given: """ 368 | Customer's number_of_items_bought is equal 5 369 | """, 370 | then: """ 371 | Customer's discount_percentage is 0.2 372 | """ 373 | ), 374 | NeuralBridge.Rule.new( 375 | id: 1, 376 | given: """ 377 | Customer's number_of_items_bought is lesser 2 378 | """, 379 | then: """ 380 | Customer's discount_percentage is 0.0 381 | """ 382 | ), 383 | NeuralBridge.Rule.new( 384 | id: 1, 385 | given: """ 386 | Customer's number_of_items_bought is equal 3 387 | """, 388 | then: """ 389 | Customer's discount_percentage is 0.1 390 | """ 391 | ) 392 | ] 393 | 394 | facts = """ 395 | Customer's number_of_items_bought is 5 396 | """ 397 | 398 | [ 399 | %Retex.Wme{ 400 | identifier: "Customer", 401 | attribute: "discount_percentage", 402 | value: 0.2 403 | } 404 | ] = 405 | NeuralBridge.Session.new("uk") 406 | |> NeuralBridge.Session.add_rules(rules) 407 | |> NeuralBridge.Session.add_facts(facts) 408 | |> Map.fetch!(:inferred_facts) 409 | ``` 410 | 411 | ## Example 3: loan approval 412 | 413 | ```elixir 414 | rules = [ 415 | NeuralBridge.Rule.new( 416 | id: 1, 417 | given: """ 418 | Applicant's credit_score is greater 700 419 | """, 420 | then: """ 421 | Loan's approval_status is "approved" 422 | """ 423 | ), 424 | NeuralBridge.Rule.new( 425 | id: 2, 426 | given: """ 427 | Applicant's credit_score is lesser 500 428 | """, 429 | then: """ 430 | Customer's at_risk is true 431 | Loan's approval_status is "rejected" 432 | """ 433 | ) 434 | ] 435 | 436 | facts = """ 437 | Applicant's credit_score is 499 438 | """ 439 | 440 | [ 441 | %Retex.Wme{ 442 | identifier: "Customer", 443 | attribute: "at_risk", 444 | value: true 445 | }, 446 | %Retex.Wme{ 447 | identifier: "Loan", 448 | attribute: "approval_status", 449 | value: "rejected" 450 | } 451 | ] = 452 | NeuralBridge.Session.new("uk") 453 | |> NeuralBridge.Session.add_rules(rules) 454 | |> NeuralBridge.Session.add_facts(facts) 455 | |> Map.fetch!(:inferred_facts) 456 | ``` 457 | 458 | ## Example 3: workflow automation 459 | 460 | ```elixir 461 | rules = [ 462 | NeuralBridge.Rule.new( 463 | id: 1, 464 | given: """ 465 | SupportTicket's opening_time_hours greater 24 466 | SupportTicket's id is equal $ticke_id 467 | """, 468 | then: """ 469 | SupportTicket's escalation_level "high" 470 | SupportTicket's escalated is $ticke_id 471 | """ 472 | ) 473 | ] 474 | 475 | facts = """ 476 | SupportTicket's opening_time_hours is 25 477 | SupportTicket's id is "123AB_ID" 478 | """ 479 | 480 | [ 481 | %Retex.Wme{ 482 | identifier: "SupportTicket", 483 | attribute: "escalation_level", 484 | value: "high" 485 | }, 486 | %Retex.Wme{ 487 | identifier: "SupportTicket", 488 | attribute: "escalated", 489 | value: "123AB_ID" 490 | } 491 | ] = 492 | NeuralBridge.Session.new("uk") 493 | |> NeuralBridge.Session.add_rules(rules) 494 | |> NeuralBridge.Session.add_facts(facts) 495 | |> Map.fetch!(:inferred_facts) 496 | ``` 497 | 498 | This library is just glue for two projects [Retex](https://github.com/lorenzosinisi/retex) 499 | and the DSL [Sanskrit](https://github.com/lorenzosinisi/sanskrit). Retex and Sanskrit can 500 | be put together to form an expert system in Elixir as shown in the examples above. 501 | 502 | If you have any comment or question feel free to open an issue here 503 | 504 | ### ChatGPT help 505 | 506 | You can use the following prompt for ChatGPT to help yourself create rules to train the system https://gist.github.com/lorenzosinisi/eec245aff37faf57b3a04fbaf655ea20 507 | 508 | #### References: 509 | 510 | - [Rete algorithm](https://en.wikipedia.org/wiki/Rete_algorithm) 511 | - [Expert system](https://en.wikipedia.org/wiki/Expert_system#:~:text=In%20artificial%20intelligence%2C%20an%20expert,than%20through%20conventional%20procedural%20code.) 512 | - [Forgy's paper](http://www.csl.sri.com/users/mwfong/Technical/RETE%20Match%20Algorithm%20-%20Forgy%20OCR.pdf) 513 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :neural_bridge, defined_functions: NeuralBridge.DefinedFunctions 4 | -------------------------------------------------------------------------------- /examples/example.livemd: -------------------------------------------------------------------------------- 1 | # Rule engine in Elixir - Rete 2 | 3 | ```elixir 4 | Mix.install([ 5 | {:neural_bridge, git: "https://github.com/lorenzosinisi/neural_bridge"} 6 | ]) 7 | ``` 8 | 9 | ## Example 1: calculate the net salary of an employee in the UK 10 | 11 | ```elixir 12 | rules = [ 13 | NeuralBridge.Rule.new( 14 | id: 1, 15 | given: """ 16 | Person's salary is equal $salary 17 | """, 18 | then: """ 19 | let $monthly_salary = div($salary, 12) 20 | Person's monthly_salary is $monthly_salary 21 | """ 22 | ), 23 | NeuralBridge.Rule.new( 24 | id: 2, 25 | given: """ 26 | Person's monthly_salary is equal $monthly_salary 27 | """, 28 | then: """ 29 | let $payout = mult($monthly_salary, 0.64) 30 | Salary's net_amount is $payout 31 | """ 32 | ), 33 | NeuralBridge.Rule.new( 34 | id: 2, 35 | given: """ 36 | Salary's net_amount is equal $amount 37 | """, 38 | then: """ 39 | Salary's net_amount is $amount 40 | """ 41 | ) 42 | ] 43 | 44 | facts = """ 45 | Person's salary is 60000 46 | Person's employment_type is "Full-time" 47 | Person's location is "UK" 48 | """ 49 | 50 | NeuralBridge.Session.new("uk") 51 | |> NeuralBridge.Session.add_rules(rules) 52 | |> NeuralBridge.Session.add_facts(facts) 53 | |> Map.fetch!(:inferred_facts) 54 | ``` 55 | 56 | ## Example 2: dynamic pricing 57 | 58 | In this example, the rules calculate the discount to be applied to a customer based on the number of items they have bought in a month. The discount percentages are determined as follows: 59 | 60 | * If the customer has bought 5 items, the discount percentage is set to 20%. 61 | * If the customer has bought less than 2 items, the discount percentage is set to 0%. 62 | * If the customer has bought exactly 3 items, the discount percentage is set to 10%. 63 | 64 | ```elixir 65 | rules = [ 66 | NeuralBridge.Rule.new( 67 | id: 1, 68 | given: """ 69 | Customer's number_of_items_bought is equal 5 70 | """, 71 | then: """ 72 | Customer's discount_percentage is 0.2 73 | """ 74 | ), 75 | NeuralBridge.Rule.new( 76 | id: 1, 77 | given: """ 78 | Customer's number_of_items_bought is lesser 2 79 | """, 80 | then: """ 81 | Customer's discount_percentage is 0.0 82 | """ 83 | ), 84 | NeuralBridge.Rule.new( 85 | id: 1, 86 | given: """ 87 | Customer's number_of_items_bought is equal 3 88 | """, 89 | then: """ 90 | Customer's discount_percentage is 0.1 91 | """ 92 | ) 93 | ] 94 | 95 | facts = """ 96 | Customer's number_of_items_bought is 5 97 | """ 98 | 99 | [ 100 | %Retex.Wme{ 101 | identifier: "Customer", 102 | attribute: "discount_percentage", 103 | value: 0.2 104 | } 105 | ] = 106 | NeuralBridge.Session.new("uk") 107 | |> NeuralBridge.Session.add_rules(rules) 108 | |> NeuralBridge.Session.add_facts(facts) 109 | |> Map.fetch!(:inferred_facts) 110 | ``` 111 | 112 | ## Example 3: loan approval 113 | 114 | ```elixir 115 | rules = [ 116 | NeuralBridge.Rule.new( 117 | id: 1, 118 | given: """ 119 | Applicant's credit_score is greater 700 120 | """, 121 | then: """ 122 | Loan's approval_status is "approved" 123 | """ 124 | ), 125 | NeuralBridge.Rule.new( 126 | id: 2, 127 | given: """ 128 | Applicant's credit_score is lesser 500 129 | """, 130 | then: """ 131 | Customer's at_risk is true 132 | Loan's approval_status is "rejected" 133 | """ 134 | ) 135 | ] 136 | 137 | facts = """ 138 | Applicant's credit_score is 499 139 | """ 140 | 141 | [ 142 | %Retex.Wme{ 143 | identifier: "Customer", 144 | attribute: "at_risk", 145 | value: true 146 | }, 147 | %Retex.Wme{ 148 | identifier: "Loan", 149 | attribute: "approval_status", 150 | value: "rejected" 151 | } 152 | ] = 153 | NeuralBridge.Session.new("uk") 154 | |> NeuralBridge.Session.add_rules(rules) 155 | |> NeuralBridge.Session.add_facts(facts) 156 | |> Map.fetch!(:inferred_facts) 157 | ``` 158 | 159 | ## Example 3: workflow automation 160 | 161 | ```elixir 162 | rules = [ 163 | NeuralBridge.Rule.new( 164 | id: 1, 165 | given: """ 166 | SupportTicket's opening_time_hours greater 24 167 | SupportTicket's id is equal $ticke_id 168 | """, 169 | then: """ 170 | SupportTicket's escalation_level "high" 171 | SupportTicket's escalated is $ticke_id 172 | """ 173 | ) 174 | ] 175 | 176 | facts = """ 177 | SupportTicket's opening_time_hours is 25 178 | SupportTicket's id is "123AB_ID" 179 | """ 180 | 181 | [ 182 | %Retex.Wme{ 183 | identifier: "SupportTicket", 184 | attribute: "escalation_level", 185 | value: "high" 186 | }, 187 | %Retex.Wme{ 188 | identifier: "SupportTicket", 189 | attribute: "escalated", 190 | value: "123AB_ID" 191 | } 192 | ] = 193 | NeuralBridge.Session.new("uk") 194 | |> NeuralBridge.Session.add_rules(rules) 195 | |> NeuralBridge.Session.add_facts(facts) 196 | |> Map.fetch!(:inferred_facts) 197 | ``` 198 | -------------------------------------------------------------------------------- /lib/neural_bridge.ex: -------------------------------------------------------------------------------- 1 | defmodule NeuralBridge do 2 | end 3 | -------------------------------------------------------------------------------- /lib/neural_bridge/application.ex: -------------------------------------------------------------------------------- 1 | defmodule NeuralBridge.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | def start(_type, _args) do 9 | children = [ 10 | # Starts a worker by calling: NeuralBridge.Worker.start_link(arg) 11 | # {NeuralBridge.Worker, arg} 12 | ] 13 | 14 | # See https://hexdocs.pm/elixir/Supervisor.html 15 | # for other strategies and supported options 16 | opts = [strategy: :one_for_one, name: NeuralBridge.Supervisor] 17 | Supervisor.start_link(children, opts) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/neural_bridge/function.ex: -------------------------------------------------------------------------------- 1 | defmodule NeuralBridge.Function do 2 | @moduledoc false 3 | require Decimal 4 | require Logger 5 | 6 | defstruct variable_name: nil, 7 | function_name: nil, 8 | arguments: [], 9 | # The module that implements Retex.FunctionBehaviour 10 | functions_mod: nil 11 | 12 | def new(variable_name, function_name, args) 13 | when is_binary(variable_name) and is_binary(function_name) and is_list(args) do 14 | %__MODULE__{variable_name: variable_name, function_name: function_name, arguments: args} 15 | end 16 | 17 | def call(name, args, bindings, function_mod) when is_binary(name) when is_list(args) do 18 | args = Enum.map(args, fn variable -> Map.get(bindings, variable, variable) end) 19 | 20 | try do 21 | post_process(function_mod.call(name, pre_process_args(args))) 22 | rescue 23 | e -> 24 | message = 25 | "Error while executing #{name} with args #{inspect(args)}" <> Exception.message(e) 26 | 27 | Logger.error(message) 28 | 29 | "ERROR" 30 | end 31 | end 32 | 33 | def pre_process_args(args) do 34 | Enum.map(args, fn arg -> arg end) 35 | end 36 | 37 | defp post_process(value) do 38 | cond do 39 | Decimal.is_decimal(value) -> 40 | Decimal.to_float(value) 41 | 42 | true -> 43 | value 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/neural_bridge/function/defined_functions.ex: -------------------------------------------------------------------------------- 1 | defmodule NeuralBridge.DefinedFunctions do 2 | @behaviour NeuralBridge.FunctionBehaviour 3 | alias Decimal 4 | require Decimal 5 | 6 | @impl NeuralBridge.FunctionBehaviour 7 | def call("div", [a, b]) do 8 | Decimal.div(Decimal.new(a), Decimal.new(b)) 9 | end 10 | 11 | def call("compare", [a, b]) do 12 | Decimal.compare(Decimal.new(a), Decimal.new(b)) 13 | end 14 | 15 | def call("equal", [a, b]) do 16 | Decimal.equal?(Decimal.new(a), Decimal.new(b)) 17 | end 18 | 19 | def call("add", [a, b]) do 20 | Decimal.add(Decimal.new(a), Decimal.new(b)) 21 | end 22 | 23 | def call("div_int", [a, b]) do 24 | Decimal.div_int(Decimal.new(a), Decimal.new(b)) 25 | end 26 | 27 | def call("div_rem", [a, b]) do 28 | Decimal.div_int(Decimal.new(a), Decimal.new(b)) 29 | end 30 | 31 | def call("is_decimal", [a]) do 32 | Decimal.is_decimal(a) 33 | end 34 | 35 | def call("min", [a, b]) do 36 | min(a, b) 37 | end 38 | 39 | def call("max", [a, b]) do 40 | max(a, b) 41 | end 42 | 43 | def call("mult", [a, b]) do 44 | Decimal.mult(Decimal.new("#{a}"), Decimal.new("#{b}")) 45 | end 46 | 47 | def call("round", [a]) do 48 | Decimal.round(Decimal.new(a)) 49 | end 50 | 51 | def call("round", [num, places]) when is_binary(num) do 52 | Decimal.round(Decimal.new(num), places) 53 | end 54 | 55 | def call("round", [num, places]) when is_float(num) do 56 | Decimal.round(Decimal.from_float(num), places) 57 | end 58 | 59 | def call("round", [num, places, mode]) when is_float(num) do 60 | Decimal.round(Decimal.from_float(num), places, String.to_existing_atom(mode)) 61 | end 62 | 63 | def call("round", [num, places, mode]) when is_binary(num) do 64 | Decimal.round(Decimal.new(num), places, String.to_existing_atom(mode)) 65 | end 66 | 67 | def call("to_string", [num, mode]) when is_binary(num) do 68 | Decimal.to_string(Decimal.new(num), String.to_existing_atom(mode)) 69 | end 70 | 71 | def call("to_string", [num, mode]) when is_float(num) do 72 | Decimal.to_string(Decimal.from_float(num), String.to_existing_atom(mode)) 73 | end 74 | 75 | def call("to_string", [num]) when is_float(num) do 76 | Decimal.to_string(Decimal.from_float(num)) 77 | end 78 | 79 | def call("abs", [a]) when is_binary(a) do 80 | Decimal.abs(Decimal.new(a)) 81 | end 82 | 83 | def call("abs", [a]) when is_float(a) do 84 | Decimal.abs(Decimal.from_float(a)) 85 | end 86 | 87 | def call("abs", [a]) when is_number(a) do 88 | Decimal.abs(Decimal.new("#{a}")) 89 | end 90 | 91 | def call(name, args) do 92 | raise "Undefined function #{name} with args #{inspect(args)}" 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/neural_bridge/function/function_behaviour.ex: -------------------------------------------------------------------------------- 1 | defmodule NeuralBridge.FunctionBehaviour do 2 | @callback call(function_name :: String, arguments :: list(any())) :: any() 3 | end 4 | -------------------------------------------------------------------------------- /lib/neural_bridge/rule.ex: -------------------------------------------------------------------------------- 1 | defmodule NeuralBridge.Rule do 2 | @moduledoc false 3 | import NeuralBridge.SanskritInterpreter 4 | defstruct [:id, :given, :then] 5 | require Logger 6 | 7 | defmodule Error do 8 | defexception [:message] 9 | end 10 | 11 | def new(id: id, given: given, then: then) when is_binary(given) and is_binary(then) do 12 | with {:ok, parsed_given} <- to_prod(given, id), 13 | _ <- validate_given(parsed_given, given, id), 14 | {:ok, then} <- 15 | to_prod(then, id) do 16 | %__MODULE__{id: id, given: parsed_given, then: then} 17 | end 18 | end 19 | 20 | def new(id: id, given: given, then: then) when is_binary(given) and is_function(then) do 21 | with {:ok, given_prod} <- to_prod(given, id), 22 | _ <- validate_given(given_prod, given, id) do 23 | %__MODULE__{id: id, given: given_prod, then: then} 24 | end 25 | end 26 | 27 | defp to_prod(given, id) do 28 | parsed = 29 | given 30 | |> String.split("\n") 31 | |> Enum.map(fn stm -> 32 | case to_production(stm) do 33 | {:error, {statement_error, _}} -> 34 | raise Error, message: "Error at rule #{id} - Invalid statement: " <> statement_error 35 | 36 | {:ok, parsed} -> 37 | parsed 38 | end 39 | end) 40 | |> List.flatten() 41 | 42 | {:ok, parsed} 43 | end 44 | 45 | defp validate_given(given, original_given, id) when is_list(given) do 46 | Enum.map(given, fn statement -> 47 | unless Retex.Protocol.AlphaNetwork.impl_for(statement) do 48 | line = Enum.find_index(given, fn element -> element == statement end) 49 | statement = String.split(original_given, "\n") |> Enum.at(line) 50 | {_, impls} = Retex.Protocol.AlphaNetwork.__protocol__(:impls) 51 | 52 | Logger.error(""" 53 | Allowed facts in a given of a rule are: #{inspect(impls)} 54 | """) 55 | 56 | raise Error, 57 | message: 58 | "Invalid statement in the given of the rule id #{id} at line #{line + 1}: #{statement}" 59 | end 60 | end) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/neural_bridge/sanskrit_interpreter.ex: -------------------------------------------------------------------------------- 1 | defmodule NeuralBridge.SanskritInterpreter do 2 | @moduledoc """ 3 | A local DSL interpreter for the Elixir https://github.com/lorenzosinisi/sanskrit 4 | """ 5 | import Retex.Facts 6 | 7 | @spec to_production(binary()) :: {:ok, list(Retex.Fact.t())} | {:error, any()} 8 | def to_production(conditions) when is_binary(conditions) do 9 | with {:ok, ast} <- parse(conditions) do 10 | {:ok, interpret(ast)} 11 | end 12 | end 13 | 14 | @spec to_production!(binary()) :: list(Retex.Fact.t()) 15 | def to_production!(conditions) when is_binary(conditions) do 16 | {:ok, ast} = to_production(conditions) 17 | ast 18 | end 19 | 20 | defp parse(str) do 21 | case Sanskrit.parse(str) do 22 | {:ok, _} = result -> result 23 | {:error, error} -> {:error, {str, error}} 24 | end 25 | end 26 | 27 | defp interpret(ast) when is_list(ast) do 28 | for node <- ast, do: do_interpret(node) 29 | end 30 | 31 | defp do_interpret({:filter, type, kind, value}) do 32 | filter(type, kind, value) 33 | end 34 | 35 | defp do_interpret({:fun, variable, function_name, bindings}) do 36 | NeuralBridge.Function.new(variable, function_name, bindings) 37 | end 38 | 39 | defp do_interpret({:negation, variable, type}) do 40 | is_not(variable, type) 41 | end 42 | 43 | defp do_interpret({:wme, type, attr, value}) do 44 | Retex.Wme.new(type, attr, value) 45 | end 46 | 47 | defp do_interpret({:isa, var, type}) do 48 | isa(var, type) 49 | end 50 | 51 | defp do_interpret({:has_attribute, type, attr, kind, value}) do 52 | has_attribute(type, attr, kind, value) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/neural_bridge/session.ex: -------------------------------------------------------------------------------- 1 | defmodule NeuralBridge.Session do 2 | @moduledoc """ 3 | Handles a single session for inference with the rule engine 4 | """ 5 | require Logger 6 | alias __MODULE__ 7 | 8 | defmodule Error do 9 | defexception [:message] 10 | end 11 | 12 | defmodule Solution do 13 | @moduledoc false 14 | defstruct text: "", bindings: %{} 15 | end 16 | 17 | @defined_functions Application.compile_env!(:neural_bridge, :defined_functions) 18 | 19 | defstruct rule_engine: nil, 20 | id: nil, 21 | rules_fired: [], 22 | name: nil, 23 | solution: [], 24 | inferred_facts: [], 25 | errors: [], 26 | functions_mod: @defined_functions 27 | 28 | @type t :: %__MODULE__{rule_engine: any(), id: String.t(), rules_fired: list(rule)} 29 | @type rule :: %{id: String.t(), given: Retex.Facts.t(), then: list(Retex.Facts.t() | any())} 30 | 31 | @spec new(String.t()) :: t() 32 | def new(id), do: %Session{id: id, rule_engine: Retex.new()} 33 | 34 | @doc "Merge the pre-existing rules with the new set of rules provided" 35 | @spec add_rules(t(), list(rule)) :: t() 36 | def add_rules(session = %__MODULE__{rule_engine: rule_engine}, rules) 37 | when is_list(rules) do 38 | validate_rules(rules) 39 | rule_engine = Enum.reduce(rules, rule_engine, &add_production(&2, &1)) 40 | %{session | rule_engine: rule_engine} 41 | end 42 | 43 | defp validate_rules(rules) do 44 | Enum.map(rules, fn rule -> 45 | case rule do 46 | {:error, error} -> 47 | raise Error, message: error 48 | 49 | any -> 50 | any 51 | end 52 | end) 53 | end 54 | 55 | defp add_production(engine, rule) do 56 | Retex.add_production(engine, rule) 57 | end 58 | 59 | @doc "Return the reason why a rule would be activated" 60 | @spec why(t(), map()) :: Retex.Why.t() 61 | def why(%__MODULE__{rule_engine: rule_engine}, node) do 62 | Retex.Why.explain(rule_engine, node) 63 | end 64 | 65 | def add_facts(session = %__MODULE__{rule_engine: rule_engine}, facts) when is_binary(facts) do 66 | ast_facts = NeuralBridge.SanskritInterpreter.to_production!(facts) 67 | new_rule_engine = Enum.reduce(ast_facts, rule_engine, &Retex.add_wme(&2, &1)) 68 | new_session = %{session | rule_engine: new_rule_engine} 69 | apply_rules(new_session) 70 | end 71 | 72 | def add_facts(session = %__MODULE__{rule_engine: rule_engine}, facts) when is_list(facts) do 73 | new_rule_engine = Enum.reduce(facts, rule_engine, &Retex.add_wme(&2, &1)) 74 | new_session = %{session | rule_engine: new_rule_engine} 75 | 76 | apply_rules(new_session) 77 | end 78 | 79 | def add_facts(session = %__MODULE__{}, %Retex.Wme{} = wme) do 80 | add_facts(session, [wme]) 81 | end 82 | 83 | @doc "Take the list of applicable rules from the agenda and trigger them if they haven't been applied yet" 84 | @spec apply_rules(t()) :: t() 85 | def apply_rules(session = %__MODULE__{rule_engine: rule_engine, rules_fired: rules_fired}) do 86 | rules = extract_applicable_rules(rule_engine, rules_fired) 87 | updated_session = Enum.reduce(rules, session, &apply_rule(&2, &1)) 88 | if Enum.any?(rules), do: apply_rules(updated_session), else: updated_session 89 | end 90 | 91 | defp extract_applicable_rules(%_{agenda: agenda}, rules_fired) do 92 | agenda -- rules_fired 93 | end 94 | 95 | defp apply_rule( 96 | session = %__MODULE__{rules_fired: rules_fired}, 97 | rule = %_{action: actions, bindings: bindings} 98 | ) do 99 | {updated_session, _bindings} = 100 | Enum.reduce(actions, {session, bindings}, &do_apply_rule(&2, &1, rule)) 101 | 102 | %{ 103 | updated_session 104 | | solution: order_solutions(updated_session), 105 | rules_fired: List.flatten([rule | rules_fired]) 106 | } 107 | end 108 | 109 | defp order_solutions(%__MODULE__{solution: solutions}) do 110 | Enum.sort_by(solutions, fn solution -> map_size(solution.bindings) end) 111 | end 112 | 113 | defp do_apply_rule( 114 | {session = %__MODULE__{}, bindings}, 115 | function = %NeuralBridge.Function{}, 116 | _rule = %{} 117 | ) do 118 | result = 119 | NeuralBridge.Function.call( 120 | function.function_name, 121 | function.arguments, 122 | bindings, 123 | session.functions_mod 124 | ) 125 | 126 | {session, Map.put_new(bindings, function.variable_name, result)} 127 | end 128 | 129 | defp do_apply_rule( 130 | {session = %__MODULE__{}, bindings}, 131 | wme = %Retex.Wme{}, 132 | _rule = %{} 133 | ) do 134 | %{rule_engine: rule_engine} = session 135 | 136 | populated = 137 | for {key, val} <- Map.from_struct(wme), into: %{} do 138 | val = 139 | case val do 140 | "$" <> variable_name -> 141 | Map.get(bindings, "$" <> variable_name) 142 | 143 | otherwise -> 144 | otherwise 145 | end 146 | 147 | {key, val} 148 | end 149 | 150 | wme = struct(Retex.Wme, populated) 151 | rule_engine = Retex.add_wme(rule_engine, wme) 152 | 153 | {%{ 154 | session 155 | | rule_engine: rule_engine, 156 | inferred_facts: Enum.uniq(session.inferred_facts ++ [wme]) 157 | }, bindings} 158 | end 159 | 160 | defp do_apply_rule( 161 | {session = %__MODULE__{}, bindings}, 162 | _wme = {ident, attr, value}, 163 | _rule 164 | ) do 165 | %{rule_engine: rule_engine} = session 166 | rule_engine = Retex.add_wme(rule_engine, Retex.Wme.new(ident, attr, value)) 167 | 168 | {%{session | rule_engine: rule_engine}, bindings} 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule NeuralBridge.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :neural_bridge, 7 | version: "0.0.2", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | ddocs: [extras: ["README.md"], main: "readme", source_ref: "v0.0.1"], 11 | source_url: "https://github.com/lorenzosinisi/neural_bridge", 12 | description: description(), 13 | package: package(), 14 | deps: deps() 15 | ] 16 | end 17 | 18 | defp description do 19 | """ 20 | Expert system in Elixir - a bridge brain-computer-machine to transfer knowledge from an expert to an algorithm 21 | 22 | This system implements a rules based mechanism that mimics the human interaction 23 | with an expert of a specific domain. 24 | """ 25 | end 26 | 27 | # Run "mix help compile.app" to learn about applications. 28 | def application do 29 | [ 30 | extra_applications: [:logger], 31 | mod: {NeuralBridge.Application, []} 32 | ] 33 | end 34 | 35 | defp package do 36 | [ 37 | files: ["lib", "mix.exs", "README.md", "LICENSE"], 38 | maintainers: ["Lorenzo Sinisi"], 39 | maintainers: ["Lorenzo Sinisi"], 40 | licenses: ["Apache 2.0"], 41 | links: %{"GitHub" => "https://github.com/lorenzosinisi/neural_bridge"} 42 | ] 43 | end 44 | 45 | defp deps do 46 | [ 47 | {:sanskrit, "~> 0.1.8"}, 48 | {:retex, "~> 0.1.9"}, 49 | {:decimal, "~> 2.0"}, 50 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 51 | ] 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, 3 | "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"}, 5 | "ex_doc": {:hex, :ex_doc, "0.30.3", "bfca4d340e3b95f2eb26e72e4890da83e2b3a5c5b0e52607333bf5017284b063", [: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", "fbc8702046c1d25edf79de376297e608ac78cdc3a29f075484773ad1718918b6"}, 6 | "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, 7 | "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"}, 8 | "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"}, 9 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, 10 | "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, 11 | "retex": {:hex, :retex, "0.1.12", "3609705d5117f61e092cf96a98424087c5dfa1d09d8a62929ca9f72b8c9e43de", [:mix], [{:libgraph, "~> 0.16.0", [hex: :libgraph, repo: "hexpm", optional: false]}, {:uuid_tools, "~> 0.1.0", [hex: :uuid_tools, repo: "hexpm", optional: false]}], "hexpm", "6cb6cb4a8f6cfdbbd7cb625dc0d4351fe9d7ffec4856e50c3505411483063cbe"}, 12 | "sanskrit": {:hex, :sanskrit, "0.1.8", "3dc37ea7b068b09c5ca4c3d7582d811296f0dd2f607346ff11bf0e56ad4a2a63", [:mix], [{:combine, "~> 0.10.0", [hex: :combine, repo: "hexpm", optional: false]}], "hexpm", "4a15aaa1ba7f971d01d3f73041b1a5c9b474a0b8210f38da1f778443d01f62d6"}, 13 | "uuid_tools": {:hex, :uuid_tools, "0.1.1", "e94e05551dcfd085a127c11112b0e6d24a98963659bf2f16615745c492e00e38", [:mix], [], "hexpm", "eafa199917316fece589c8768427c0bac6d6959a523482d130f1997a674c0da4"}, 14 | } 15 | -------------------------------------------------------------------------------- /test/engine_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NeuralBridge.SessionTest do 2 | use ExUnit.Case 3 | alias NeuralBridge.Session 4 | alias NeuralBridge.Rule 5 | 6 | test "can be started" do 7 | assert Session.new("test") 8 | end 9 | 10 | test "can use inferred rules" do 11 | session = Session.new("doctor_AI") 12 | 13 | session = 14 | Session.add_rules(session, [ 15 | Rule.new( 16 | id: 1, 17 | given: """ 18 | Patient's fever is greater 38.5 19 | Patient's name is equal $name 20 | Patient's generic_weakness is equal "Yes" 21 | """, 22 | then: """ 23 | Patient's diagnosis is "flu" 24 | """ 25 | ), 26 | Rule.new( 27 | id: 2, 28 | given: """ 29 | Patient's diagnosis is equal "flu" 30 | """, 31 | then: """ 32 | Patient's therapy is "Aspirin" 33 | """ 34 | ), 35 | Rule.new( 36 | id: 3, 37 | given: """ 38 | Patient's therapy is equal "Aspirin" 39 | """, 40 | then: """ 41 | Patient's health_status is "recovered" 42 | """ 43 | ) 44 | ]) 45 | 46 | session = 47 | Session.add_facts(session, """ 48 | Patient's fever is 39 49 | Patient's name is "Aylon" 50 | Patient's generic_weakness is "Yes" 51 | """) 52 | 53 | session = Session.apply_rules(session) 54 | 55 | ## contains Patient's diagnosis 56 | assert [ 57 | %_{ 58 | action: [ 59 | %Retex.Wme{ 60 | identifier: "Patient", 61 | attribute: "health_status", 62 | value: "recovered" 63 | } 64 | ], 65 | bindings: %{} 66 | }, 67 | %_{ 68 | action: [ 69 | %Retex.Wme{ 70 | identifier: "Patient", 71 | attribute: "therapy", 72 | value: "Aspirin" 73 | } 74 | ], 75 | bindings: %{} 76 | }, 77 | %_{ 78 | action: [ 79 | %Retex.Wme{ 80 | identifier: "Patient", 81 | attribute: "diagnosis", 82 | value: "flu" 83 | } 84 | ], 85 | bindings: %{"$name" => "Aylon"} 86 | } 87 | ] = session.rule_engine.agenda 88 | end 89 | 90 | test "can calculate the net taxaction in the UK" do 91 | rules = [ 92 | Rule.new( 93 | id: 1, 94 | given: """ 95 | Person's salary is equal $salary 96 | Person's salary is greater 0 97 | Person's employment_type is equal "Full-time" 98 | Person's location is equal "UK" 99 | """, 100 | then: """ 101 | let $monthly_salary = div($salary, 12) 102 | Person's monthly_salary is $monthly_salary 103 | """ 104 | ), 105 | Rule.new( 106 | id: 2, 107 | given: """ 108 | Person's monthly_salary is equal $monthly_salary 109 | """, 110 | then: """ 111 | let $payout = mult($monthly_salary, 0.64) 112 | Salary's net_amount is $payout 113 | """ 114 | ), 115 | Rule.new( 116 | id: 2, 117 | given: """ 118 | Salary's net_amount is equal $amount 119 | """, 120 | then: """ 121 | Salary's net_amount is $amount 122 | """ 123 | ) 124 | ] 125 | 126 | facts = """ 127 | Person's salary is 60000 128 | Person's employment_type is "Full-time" 129 | Person's location is "UK" 130 | """ 131 | 132 | inference = 133 | NeuralBridge.Session.new("uk") 134 | |> NeuralBridge.Session.add_rules(rules) 135 | |> NeuralBridge.Session.add_facts(facts) 136 | 137 | assert [ 138 | %Retex.Wme{ 139 | attribute: "monthly_salary", 140 | id: 3_510_723_950, 141 | identifier: "Person", 142 | timestamp: nil, 143 | value: 5000.0 144 | }, 145 | %Retex.Wme{ 146 | attribute: "net_amount", 147 | id: 1_705_982_201, 148 | identifier: "Salary", 149 | timestamp: nil, 150 | value: 3200.0 151 | }, 152 | %Retex.Wme{ 153 | attribute: "net_amount", 154 | id: 1_238_196_029, 155 | identifier: "Salary", 156 | timestamp: nil, 157 | value: 3200.0 158 | } 159 | ] = Map.fetch!(inference, :inferred_facts) 160 | end 161 | 162 | test "can apply all defined functions" do 163 | rules = [ 164 | Rule.new( 165 | id: 1, 166 | given: """ 167 | Person's salary is equal $salary 168 | """, 169 | then: """ 170 | let $div = div($salary, 12) 171 | let $mult = mult($salary, 12) 172 | let $compare = compare($salary, 12) 173 | let $equal = equal($salary, 12) 174 | let $add = add($salary, 12) 175 | let $div_int = div_int($salary, 12) 176 | let $div_rem = div_rem($salary, 12) 177 | let $div_rem = div_rem($salary, 12) 178 | let $div_rem_neg = div_rem($salary, -12) 179 | let $is_decimal = is_decimal($salary) 180 | let $is_decimal = is_decimal(0.5) 181 | let $min = min(0.5, 0) 182 | let $round = round(0.5, 10) 183 | let $round = round(0.5, 0, "half_up") 184 | let $to_string = to_string(0.5) 185 | let $abs = abs(-0.5) 186 | Result's div is $div 187 | Result's mult is $mult 188 | Result's div_rem_neg is $div_rem_neg 189 | Result's compare is $compare 190 | Result's equal is $equal 191 | Result's div_int is $div_int 192 | Result's div_rem is $div_rem 193 | Result's is_decimal is $is_decimal 194 | Result's min is $min 195 | Result's round is $round 196 | Result's to_string is $to_string 197 | Result's abs is $abs 198 | """ 199 | ) 200 | ] 201 | 202 | facts = """ 203 | Person's salary is 60000 204 | """ 205 | 206 | inference = 207 | NeuralBridge.Session.new("uk") 208 | |> NeuralBridge.Session.add_rules(rules) 209 | |> NeuralBridge.Session.add_facts(facts) 210 | 211 | assert [ 212 | %Retex.Wme{ 213 | attribute: "div", 214 | id: 286_302_541, 215 | identifier: "Result", 216 | timestamp: nil, 217 | value: 5000.0 218 | }, 219 | %Retex.Wme{ 220 | attribute: "mult", 221 | id: 1_114_257_588, 222 | identifier: "Result", 223 | timestamp: nil, 224 | value: 720_000.0 225 | }, 226 | %Retex.Wme{ 227 | attribute: "div_rem_neg", 228 | id: 3_615_986_300, 229 | identifier: "Result", 230 | timestamp: nil, 231 | value: -5000.0 232 | }, 233 | %Retex.Wme{ 234 | attribute: "compare", 235 | id: 2_708_243_500, 236 | identifier: "Result", 237 | timestamp: nil, 238 | value: :gt 239 | }, 240 | %Retex.Wme{ 241 | attribute: "equal", 242 | id: 3_946_451_889, 243 | identifier: "Result", 244 | timestamp: nil, 245 | value: false 246 | }, 247 | %Retex.Wme{ 248 | attribute: "div_int", 249 | id: 1_371_952_866, 250 | identifier: "Result", 251 | timestamp: nil, 252 | value: 5000.0 253 | }, 254 | %Retex.Wme{ 255 | attribute: "div_rem", 256 | id: 1_404_142_044, 257 | identifier: "Result", 258 | timestamp: nil, 259 | value: 5000.0 260 | }, 261 | %Retex.Wme{ 262 | attribute: "is_decimal", 263 | id: 1_305_437_226, 264 | identifier: "Result", 265 | timestamp: nil, 266 | value: false 267 | }, 268 | %Retex.Wme{ 269 | attribute: "min", 270 | id: 3_799_248_231, 271 | identifier: "Result", 272 | timestamp: nil, 273 | value: 0 274 | }, 275 | %Retex.Wme{ 276 | attribute: "round", 277 | id: 1_729_796_609, 278 | identifier: "Result", 279 | timestamp: nil, 280 | value: 0.5 281 | }, 282 | %Retex.Wme{ 283 | attribute: "to_string", 284 | id: 1_899_589_166, 285 | identifier: "Result", 286 | timestamp: nil, 287 | value: "0.5" 288 | }, 289 | %Retex.Wme{ 290 | attribute: "abs", 291 | id: 2_853_144_464, 292 | identifier: "Result", 293 | timestamp: nil, 294 | value: 0.5 295 | } 296 | ] = Map.fetch!(inference, :inferred_facts) 297 | end 298 | end 299 | -------------------------------------------------------------------------------- /test/integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NeuralBridge.IntegrationTest do 2 | use ExUnit.Case 3 | import ExUnit.CaptureLog 4 | 5 | test "returns an error if the given of a rule is invalid" do 6 | assert capture_log(fn -> 7 | assert_raise NeuralBridge.Rule.Error, 8 | ~r/Invalid statement in the given of the rule id 1 at line 2: Customer's number_of_items_bought is 5/, 9 | fn -> 10 | [ 11 | NeuralBridge.Rule.new( 12 | id: 1, 13 | given: """ 14 | Customer's number_of_items_bought is equal 5 15 | Customer's number_of_items_bought is 5 16 | """, 17 | then: """ 18 | Customer's discount_percentage is 0.2 19 | """ 20 | ) 21 | ] 22 | end 23 | end) =~ 24 | "Allowed facts in a given of a rule are: [Retex.Fact.HasAttribute, Retex.Fact.IsNot, Retex.Fact.Isa, Retex.Fact.NotExistingAttribute]" 25 | end 26 | 27 | test "invalid statement in a rule" do 28 | assert_raise NeuralBridge.Rule.Error, 29 | ~r/Error at rule 1 - Invalid statement: Customer's number_of _items_ bought is equal 5/, 30 | fn -> 31 | [ 32 | NeuralBridge.Rule.new( 33 | id: 1, 34 | given: """ 35 | Customer's number_of _items_ bought is equal 5 36 | Customer's numbe r_of_item s_bought is ciao 5 37 | """, 38 | then: """ 39 | Customer's discount_percentage is 0.2 40 | """ 41 | ) 42 | ] 43 | end 44 | end 45 | 46 | test "dynamic pricing" do 47 | rules = [ 48 | NeuralBridge.Rule.new( 49 | id: 1, 50 | given: """ 51 | Customer's number_of_items_bought is equal 5 52 | """, 53 | then: """ 54 | Customer's discount_percentage is 0.2 55 | """ 56 | ), 57 | NeuralBridge.Rule.new( 58 | id: 1, 59 | given: """ 60 | Customer's number_of_items_bought is lesser 2 61 | """, 62 | then: """ 63 | Customer's discount_percentage is 0.0 64 | """ 65 | ), 66 | NeuralBridge.Rule.new( 67 | id: 1, 68 | given: """ 69 | Customer's number_of_items_bought is equal 3 70 | """, 71 | then: """ 72 | Customer's discount_percentage is 0.1 73 | """ 74 | ) 75 | ] 76 | 77 | facts = """ 78 | Customer's number_of_items_bought is 5 79 | """ 80 | 81 | assert [ 82 | %Retex.Wme{ 83 | identifier: "Customer", 84 | attribute: "discount_percentage", 85 | value: 0.2 86 | } 87 | ] = 88 | NeuralBridge.Session.new("uk") 89 | |> NeuralBridge.Session.add_rules(rules) 90 | |> NeuralBridge.Session.add_facts(facts) 91 | |> Map.fetch!(:inferred_facts) 92 | end 93 | 94 | test "loan approval" do 95 | rules = [ 96 | NeuralBridge.Rule.new( 97 | id: 1, 98 | given: """ 99 | Applicant's credit_score is greater 700 100 | """, 101 | then: """ 102 | Loan's approval_status is "approved" 103 | """ 104 | ), 105 | NeuralBridge.Rule.new( 106 | id: 2, 107 | given: """ 108 | Applicant's credit_score is lesser 500 109 | """, 110 | then: """ 111 | Customer's at_risk is true 112 | Loan's approval_status is "rejected" 113 | """ 114 | ) 115 | ] 116 | 117 | facts = """ 118 | Applicant's credit_score is 499 119 | """ 120 | 121 | assert [ 122 | %Retex.Wme{ 123 | identifier: "Customer", 124 | attribute: "at_risk", 125 | value: true 126 | }, 127 | %Retex.Wme{ 128 | identifier: "Loan", 129 | attribute: "approval_status", 130 | value: "rejected" 131 | } 132 | ] = 133 | NeuralBridge.Session.new("uk") 134 | |> NeuralBridge.Session.add_rules(rules) 135 | |> NeuralBridge.Session.add_facts(facts) 136 | |> Map.fetch!(:inferred_facts) 137 | end 138 | 139 | test "workflow automation" do 140 | rules = [ 141 | NeuralBridge.Rule.new( 142 | id: 1, 143 | given: """ 144 | SupportTicket's opening_time_hours greater 24 145 | SupportTicket's id is equal $ticke_id 146 | """, 147 | then: """ 148 | SupportTicket's escalation_level "high" 149 | SupportTicket's escalated is $ticke_id 150 | """ 151 | ) 152 | ] 153 | 154 | facts = """ 155 | SupportTicket's opening_time_hours is 25 156 | SupportTicket's id is "123AB_ID" 157 | """ 158 | 159 | assert [ 160 | %Retex.Wme{ 161 | identifier: "SupportTicket", 162 | attribute: "escalation_level", 163 | value: "high" 164 | }, 165 | %Retex.Wme{ 166 | identifier: "SupportTicket", 167 | attribute: "escalated", 168 | value: "123AB_ID" 169 | } 170 | ] = 171 | NeuralBridge.Session.new("uk") 172 | |> NeuralBridge.Session.add_rules(rules) 173 | |> NeuralBridge.Session.add_facts(facts) 174 | |> Map.fetch!(:inferred_facts) 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /test/neural_bridge_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NeuralBridgeTest do 2 | use ExUnit.Case 3 | doctest NeuralBridge 4 | end 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------