diff --git a/process/AISalesproject_lib/AISalesproject_lib.aod b/process/AISalesproject_lib/AISalesproject_lib.aod
new file mode 100644
index 0000000000000000000000000000000000000000..c109e9524b9ff27dffb007486cdb1091ef22e50c
--- /dev/null
+++ b/process/AISalesproject_lib/AISalesproject_lib.aod
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.1">
+  <name>AISalesproject_lib</name>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <process>%aditoprj%/process/AISalesproject_lib/process.js</process>
+  <variants>
+    <element>LIBRARY</element>
+  </variants>
+</process>
diff --git a/process/AISalesproject_lib/process.js b/process/AISalesproject_lib/process.js
new file mode 100644
index 0000000000000000000000000000000000000000..0521bd2d9eb7229a5ded416377bd708fb15145dc
--- /dev/null
+++ b/process/AISalesproject_lib/process.js
@@ -0,0 +1,157 @@
+import("AttributeRegistry_basic");
+import("KeywordRegistry_basic");
+import("Sql_lib");
+import("system.logging");
+import("system.db");
+import("system.result");
+import("system.vars");
+import("Keyword_lib");
+import("AI_lib");
+import("AISalesproject_lib");
+import("Attribute_lib");
+import("system.entities");
+import("DataCaching_lib");
+
+/**
+ * Provides static methods for artificial intelligence.<br>
+ * <b>Do not create an instance of this!</b>
+ * 
+ * @class
+ */
+function AISalesprojectUtil(){}
+
+
+AISalesprojectUtil.getTrainedModel = function()
+{
+    var cache = new CachedData("SalesprojectNBData", false);
+    return cache.load(function (pTranslationNecessary, pLocale){
+        var data = AISalesprojectUtil.train();
+        return data;
+    });
+};
+
+
+AISalesprojectUtil.train = function ()
+{
+    //Trainingdata  
+    var data = new NBDataSet();
+    
+    var loadConfig = entities.createConfigForLoadingRows()
+        .entity("Salesproject_entity")
+        .fields(["#UID", "CONTACT_ID", "PHASE", "STATUS", "VOLUME", "PROBABILITY"]);
+
+    var spRecords = entities.getRows(loadConfig).map(function (spRow)
+    {
+        if(spRow["STATUS"] == $KeywordRegistry.salesprojectState$lost() || spRow["STATUS"] == $KeywordRegistry.salesprojectState$order())
+        {
+            var attributes = [];
+            attributes.push(spRow["PHASE"]) //PHASE
+            if(spRow["VOLUME"] != null && parseFloat(spRow["VOLUME"]) > 0)
+                attributes.push(AISalesprojectUtil.getVolumeClassification(parseFloat(spRow["VOLUME"]))); //VOLUME
+            if(spRow["PROBABILITY"]  != null)
+                attributes.push(AISalesprojectUtil.getProbabilityValue(spRow["PROBABILITY"])); //PROBABILITY
+            var sectorSP = new AttributeRelationQuery(spRow["CONTACT_ID"], $AttributeRegistry.industry()).getSingleAttributeValue();
+            if(sectorSP != null)
+                attributes.push(sectorSP); //Sector       
+            var doc = new NBDocument(spRow["#UID"], attributes);
+
+            var loadCompConfig = entities.createConfigForLoadingRows()
+                .entity("Competition_entity")
+                .provider("Links")
+                .addParameter("ObjectType_param", "Salesproject")
+                .addParameter("ObjectRowId_param", spRow["#UID"])
+                .fields(["ORGANISATION_NAME"]);
+
+            var compRecords = entities.getRows(loadCompConfig).map(function (compRow)
+            {
+                doc.add(compRow["ORGANISATION_NAME"]); //competitors
+            });
+
+            data.add(spRow["STATUS"], [doc]); // train model with lost and order salesprojects        
+        }
+        
+    });
+
+    // an optimisation for working with small vocabularies
+    var options = {
+        applyInverse: true
+    };
+
+    // create a classifier
+    var classifier = new NBClassifier(options);
+
+    // train the classifier
+    classifier.train(data);
+
+    //logging.log('Classifier trained.');
+    //logging.log(JSON.stringify(classifier));
+    return JSON.stringify(classifier);
+
+};
+
+AISalesprojectUtil.classify = function (pSalesprojectId, pContactId, pPhase, pStatus, pVolume, pProb)
+{
+    
+    if(pSalesprojectId == null || pContactId == null)
+        return "--";
+    var testAttributes = [];   
+    testAttributes.push(pPhase) //PHASE
+    if(pVolume != null && parseFloat(pVolume) > 0)
+        testAttributes.push(AISalesprojectUtil.getVolumeClassification(parseFloat(pVolume))); //VOLUME
+    if(pProb != null)
+        testAttributes.push(AISalesprojectUtil.getProbabilityValue(pProb)); //PROBABILITY
+    if(pContactId != null) 
+    {
+        var sectorSP = new AttributeRelationQuery(pContactId, $AttributeRegistry.industry()).getSingleAttributeValue();
+        if(sectorSP != null)
+            testAttributes.push(sectorSP); //Sector    
+    }      
+    var loadCompConfig = entities.createConfigForLoadingRows()
+        .entity("Competition_entity")
+        .provider("Links")
+        .addParameter("ObjectType_param", "Salesproject")
+        .addParameter("ObjectRowId_param", pSalesprojectId)
+        .fields(["ORGANISATION_NAME"]);
+
+    var compRecords = entities.getRows(loadCompConfig).map(function (compRow) 
+    {
+        testAttributes.push(compRow["ORGANISATION_NAME"]); //competitors
+    });
+    
+    //logging.log("testdata");
+    //logging.log(JSON.stringify(testAttributes));
+    
+    var model = AISalesprojectUtil.getTrainedModel();
+    var classifier = new NBClassifier(JSON.parse(model));
+    
+    // test the classifier on a new test object
+    var testDoc = new NBDocument(pSalesprojectId, testAttributes);
+    var classifyResult = classifier.classify(testDoc);
+    //logging.log("result");
+    //logging.log(JSON.stringify(classifyResult));
+    if(classifyResult.probability == null || isNaN(classifyResult.probability))
+        return "--";
+    else
+        return Math.round(parseFloat((classifyResult.probability) * 100)) + "% / " + KeywordUtils.getViewValue($KeywordRegistry.salesprojectState(), classifyResult.category);   
+};
+
+AISalesprojectUtil.getVolumeClassification = function (pVolume)
+{
+    if(pVolume < 100000) 
+        return "low";
+    else if(pVolume >= 100000 && pVolume < 250000)
+        return "middle";
+    else
+        return "high";
+};
+
+AISalesprojectUtil.getProbabilityValue = function (pProbId)
+{
+    if(pProbId == "SALPROJPROB0" || pProbId == "SALPROJPROB25") 
+        return "negative";
+    else if(pProbId == "SALPROJPROB50")
+        return "neutral";
+    else if(pProbId == "SALPROJPROB75" || pProbId == "SALPROJPROB100")
+        return "positive";
+    return "";
+};
\ No newline at end of file
diff --git a/process/AI_lib/AI_lib.aod b/process/AI_lib/AI_lib.aod
new file mode 100644
index 0000000000000000000000000000000000000000..153e8fc6b66bfd5d8016c8a0a22521c04ad0e209
--- /dev/null
+++ b/process/AI_lib/AI_lib.aod
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.1" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.1">
+  <name>AI_lib</name>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <process>%aditoprj%/process/AI_lib/process.js</process>
+  <variants>
+    <element>LIBRARY</element>
+  </variants>
+</process>
diff --git a/process/AI_lib/process.js b/process/AI_lib/process.js
new file mode 100644
index 0000000000000000000000000000000000000000..b0f8c0f06354aee7dd7718887cafa97f7f92b2d6
--- /dev/null
+++ b/process/AI_lib/process.js
@@ -0,0 +1,219 @@
+import("system.logging");
+import("system.vars");
+import("AI_lib");
+
+/**
+ * Provides static methods for artificial intelligence.<br>
+ * <b>Do not create an instance of this!</b>
+ * 
+ * @class
+ */
+function AIUtil(){}
+
+
+function NBClassifier(options) {
+  options = options || {};
+  this.applyInverse = options.applyInverse || false;
+  this.probabilityThreshold = options.probabilityThreshold || 0.5;
+  this.defaultCategory = options.defaultCategory || null;
+  this.tokens = options.tokens || {};
+  this.categoryCounts = options.categoryCounts || {};
+  this.probabilities = options.probabilities || {};
+}
+
+
+NBClassifier.prototype = {
+  train: function (trainingSet) {
+    var categories = Object.keys(trainingSet.categorizedItems);
+    var i = 0, j = 0, k = 0, category = "";
+    // Iterate over each category in the training set
+    for (i = 0; i < categories.length; i++) {
+      category = categories[i];
+      var subSet = trainingSet.categorizedItems[category];
+      this.categoryCounts[category] = subSet.length;
+      // Iterate over each data item in the category
+      for (j = 0; j < subSet.length; j++) {
+        var item = subSet[j];
+        // for each token in the data item, increment the token:category counter
+        var tokenlist = item.tokens;
+        for (k = 0; k < tokenlist.length; k++) {
+          var token = tokenlist[k];
+          if (!this.tokens[token]) {
+            this.tokens[token] = {};
+          }
+          if (!this.tokens[token][category]) {
+            this.tokens[token][category] = 1;
+          } else {
+            this.tokens[token][category] = 1 + this.tokens[token][category];
+          }
+        }
+      }
+    }
+    //After counting occurences of tokens, calculate probabilities.
+    for (i = 0; i < categories.length; i++) {
+      category = categories[i];
+      for (k in this.tokens) {
+        if (this.tokens.hasOwnProperty(k)) {
+          var count = this.tokens[k][category] || 0;
+          var total = this.categoryCounts[category];
+          var percentage = count / total;
+          if (!this.probabilities[category]) {
+            this.probabilities[category] = {};
+          }
+          this.probabilities[category][k] = percentage;
+        }
+      }
+    }
+  },
+
+  validate: function (testSet) {
+    var total = 0;
+    var correctGuesses = 0;
+    var wrongGuesses = 0;
+    var wrongCategories = {};
+    var wrongItems = [];
+    var categories = testSet.categorizedItems;
+    var category;
+    for (category in categories) {
+      validateCategory(category);
+    }
+
+    function validateCategory(category) {
+      if (categories.hasOwnProperty(category)) {
+        var items = categories[category];
+        var item;
+        for (item in items) {
+          if (items.hasOwnProperty(item)) {
+            total += 1;
+            item = items[item];
+            var result1 = this.classify(item);
+            // if certainty is below probabilityThreshold, go with the default
+            if (result1.probability <= this.probabilityThreshold) {
+              result1.category = this.defaultCategory || result1.category;
+            }
+            if (result1.category === category) {
+              correctGuesses++;
+            } else {
+              wrongCategories[result1.category] = (wrongCategories[result1.category]) ? wrongCategories[result1.category]++ : 1;
+              wrongItems.push(item.id);
+              wrongGuesses++;
+            }
+          }
+        }
+      }
+    }
+
+    return {
+      'total': total,
+      'correct': correctGuesses,
+      'wrong': wrongGuesses,
+      'accuracy': (correctGuesses / (correctGuesses + wrongGuesses)),
+      'wrongCategories': wrongCategories,
+      'wrongItems': wrongItems
+    };
+  },
+
+  classify: function (item) {
+    // for each category
+    var category;
+    var learnedProbabilities = this.probabilities;
+    var itemProbabilities = {};
+    var itemTokens = item.tokens;
+    for (category in learnedProbabilities) {
+      if (learnedProbabilities.hasOwnProperty(category)) {
+        itemProbabilities[category] = 1;
+        var t;
+        var probs = learnedProbabilities[category];
+        for (t in probs) {
+          // iterate over the tokens
+          if (probs.hasOwnProperty(t)) {
+            // and take the product of all probabilities
+            if (itemTokens.indexOf(t) !== -1) {
+              itemProbabilities[category] = itemProbabilities[category] * probs[t];
+            } else if (this.applyInverse) {
+              itemProbabilities[category] = itemProbabilities[category] * (1 - probs[t]);
+            }
+          }
+        }
+      }
+    }
+
+    // Pick the highest two probabilities
+    function compareCategories(a, b) {
+      if (a.probability > b.probability) {
+        return -1;
+      }
+      if (a.probability < b.probability) {
+        return 1;
+      }
+      return 0;
+    }
+
+    var categoryScores = [];
+    var sumOfProbabilities = 0;
+    var k;
+    for (k in itemProbabilities) {
+      if (itemProbabilities.hasOwnProperty(k)) {
+        categoryScores.push({
+          category: k,
+          probability: itemProbabilities[k]
+        });
+        sumOfProbabilities += itemProbabilities[k];
+      }
+    }
+    categoryScores = categoryScores.sort(compareCategories);
+
+    var firstPlace = categoryScores[0];
+    var secondPlace = categoryScores[1];
+    var timesMoreLikely = firstPlace.probability / secondPlace.probability;
+    var probability = firstPlace.probability / sumOfProbabilities;
+
+    return ({
+      'category': firstPlace.category,
+      'probability': probability,
+      'timesMoreLikely': timesMoreLikely,
+      'secondCategory': secondPlace.category,
+      'probabilities': categoryScores
+    });
+  }
+};
+
+
+function NBDataSet() {
+  this.categorizedItems = {};
+}
+
+NBDataSet.prototype = {
+  add: function (label, items) {
+    var originalItems = this.categorizedItems[label] ||  [];
+    this.categorizedItems[label] = originalItems.concat(items);
+  }
+};
+
+
+
+function NBDocument(id, tokens) {
+  if (!id) {
+    logging.log('Document(id, tokens) requires an id string');
+  }
+  this.id = id;
+  this.tokens = tokens || [];
+}
+
+NBDocument.prototype = {
+  add: function (token, factor) {
+    if(factor == undefined)
+        factor = 1
+    if (Array.isArray(token)) { // array of tokens
+      for (var i = 0; i < token.length; i++) {
+        this.add(token[i], factor);
+      }
+      return;
+    }
+    if (typeof token === 'string') {
+        for (var j = 0; j < factor; j++) {
+            this.tokens.push(token);
+        }
+    }
+  }
+};
\ No newline at end of file