You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nlpcraft.apache.org by se...@apache.org on 2021/09/30 15:27:23 UTC

[incubator-nlpcraft] 01/01: Functions enricher.

This is an automated email from the ASF dual-hosted git repository.

sergeykamov pushed a commit to branch NLPCRAFT-50-1
in repository https://gitbox.apache.org/repos/asf/incubator-nlpcraft.git

commit 3da6911ff7b2dc1c7002a14934320586fa418611
Author: Sergey Kamov <sk...@gmail.com>
AuthorDate: Thu Sep 30 15:11:36 2021 +0300

    Functions enricher.
---
 .../apache/nlpcraft/common/nlp/NCNlpSentence.scala |   2 +
 .../nlpcraft/common/nlp/NCNlpSentenceNote.scala    |   1 +
 .../org/apache/nlpcraft/model/NCModelView.java     |   6 +-
 .../apache/nlpcraft/model/impl/NCTokenLogger.scala |  16 ++-
 .../model/intent/solver/NCIntentSolver.scala       |  15 +++
 .../nlpcraft/model/tools/cmdline/NCCliBase.scala   |   2 +-
 .../org/apache/nlpcraft/probe/NCProbeBoot.scala    |   2 +
 .../nlpcraft/probe/mgrs/NCProbeVariants.scala      |   4 +-
 .../nlpcraft/probe/mgrs/NCTokenPartKey.scala       |   5 +
 .../probe/mgrs/deploy/NCDeployManager.scala        |   4 +-
 .../probe/mgrs/nlp/NCProbeEnrichmentManager.scala  |   3 +
 .../enrichers/function/NCFunctionEnricher.scala    | 114 +++++++++++++++++++++
 .../probe/mgrs/sentence/NCSentenceManager.scala    |   6 +-
 .../mgrs/nlp/enrichers/NCEnrichersTestBeans.scala  |  28 +++++
 .../function/NCEnricherFunctionSpec.scala          |  72 +++++++++++++
 15 files changed, 271 insertions(+), 9 deletions(-)

diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/nlp/NCNlpSentence.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/nlp/NCNlpSentence.scala
index 9d9f4e3..cb3f09a 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/nlp/NCNlpSentence.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/nlp/NCNlpSentence.scala
@@ -186,6 +186,8 @@ class NCNlpSentence(
                         tokensEqualOrSimilar(getList(n1, "indexes"), getList(n2, "indexes"))
                     case "nlpcraft:reference" =>
                         tokensEqualOrSimilar(getList(n1, "indexes"), getList(n2, "indexes"))
+                    case "nlpcraft:function" =>
+                        tokensEqualOrSimilar(getList(n1, "indexes"), getList(n2, "indexes"))
 
                     case _ => true
                 }
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/nlp/NCNlpSentenceNote.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/nlp/NCNlpSentenceNote.scala
index fbf4f01..9d36817 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/nlp/NCNlpSentenceNote.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/nlp/NCNlpSentenceNote.scala
@@ -280,6 +280,7 @@ object NCNlpSentenceNote {
             case "nlpcraft:relation" => Seq("type", "note") ++ addRefs("indexes")
             case "nlpcraft:sort" => Seq("asc", "subjnotes", "bynotes") ++ addRefs("subjindexes", "byindexes")
             case "nlpcraft:limit" => Seq("limit", "note") ++ addRefs("indexes", "asc") // Asc flag has sense only with references for limit.
+            case "nlpcraft:function" => Seq("type", "note") ++ addRefs("indexes")
             case "nlpcraft:coordinate" => Seq("latitude", "longitude")
             case "nlpcraft:num" => Seq("from", "to", "unit", "unitType")
             case x if x.startsWith("google:") => Seq("meta", "mentionsBeginOffsets", "mentionsContents", "mentionsTypes")
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/NCModelView.java b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/NCModelView.java
index 157a3e2..ac96b6a 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/NCModelView.java
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/NCModelView.java
@@ -296,6 +296,7 @@ public interface NCModelView extends NCMetadata {
      * <li><code>nlpcraft:relation</code></li>
      * <li><code>nlpcraft:sort</code></li>
      * <li><code>nlpcraft:limit</code></li>
+     * <li><code>nlpcraft:function</code></li>
      * </ul>
      */
     Set<String> DFLT_ENABLED_BUILTIN_TOKENS =
@@ -312,7 +313,8 @@ public interface NCModelView extends NCMetadata {
                 "nlpcraft:coordinate",
                 "nlpcraft:relation",
                 "nlpcraft:sort",
-                "nlpcraft:limit"
+                "nlpcraft:limit",
+                "nlpcraft:function"
             )
         );
 
@@ -1046,6 +1048,7 @@ public interface NCModelView extends NCMetadata {
      * <li><code>nlpcraft:relation</code></li>
      * <li><code>nlpcraft:sort</code></li>
      * <li><code>nlpcraft:limit</code></li>
+     * <li><code>nlpcraft:function</code></li> // TODO:
      * </ul>
      * Note that this method can return an empty list if the data model doesn't need any built-in tokens
      * for its logic. See {@link NCToken} for the list of all supported built-in tokens.
@@ -1216,6 +1219,7 @@ public interface NCModelView extends NCMetadata {
      *     <li><code>nlpcraft:limit</code></li>
      *     <li><code>nlpcraft:sort</code></li>
      *     <li><code>nlpcraft:relation</code></li>
+     *     <li><code>nlpcraft:function</code></li> // TODO:
      * </ul>
      * Note that entity cannot be restricted to itself (entity ID cannot appear as key as well as a
      * part of the value's set).
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/impl/NCTokenLogger.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/impl/NCTokenLogger.scala
index b3005ce..72105ca 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/impl/NCTokenLogger.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/impl/NCTokenLogger.scala
@@ -53,6 +53,7 @@ object NCTokenLogger extends LazyLogging {
         "nlpcraft:relation",
         "nlpcraft:sort",
         "nlpcraft:limit",
+        "nlpcraft:function",
         "nlpcraft:coordinate"
     )
 
@@ -85,7 +86,8 @@ object NCTokenLogger extends LazyLogging {
             "nlpcraft:date" -> Seq("from", "to", "periods"),
             "nlpcraft:relation" -> Seq("type", "indexes", "note"),
             "nlpcraft:sort" -> Seq("asc", "subjnotes", "subjindexes", "bynotes", "byindexes"),
-            "nlpcraft:limit" -> Seq("limit", "indexes", "asc", "note")
+            "nlpcraft:limit" -> Seq("limit", "indexes", "asc", "note"),
+            "nlpcraft:function" -> Seq("type", "indexes", "note")
         ).map(p => p._1 -> p._2.zipWithIndex.map(p => p._1 -> p._2).toMap)
 
     private def format(l: Long): String = new SimpleDateFormat("yyyy/MM/dd").format(new java.util.Date(l))
@@ -244,6 +246,12 @@ object NCTokenLogger extends LazyLogging {
 
                     s
 
+                case "nlpcraft:function" =>
+                    val t = mkString("type")
+                    val note = mkString("note")
+
+                    s"type=$t, indexes=[${mkIndexes("indexes")}], note=$note"
+
                 case "nlpcraft:coordinate" => s"${getValue("latitude")} and ${getValue("longitude")}"
 
                 case "nlpcraft:num" =>
@@ -522,6 +530,12 @@ object NCTokenLogger extends LazyLogging {
 
                             s
 
+                        case "nlpcraft:function" =>
+                            val t = mkString("type")
+                            val note = mkString("note")
+
+                            s"type=$t, indexes=[${getIndexes("indexes")}], note=$note"
+
                         case "nlpcraft:num" =>
                             def mkValue(name: String, fractionalField: String): String = {
                                 val d: Double = get(name)
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/solver/NCIntentSolver.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/solver/NCIntentSolver.scala
index 573ac4c..421261a 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/solver/NCIntentSolver.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/solver/NCIntentSolver.scala
@@ -298,6 +298,21 @@ class NCIntentSolver(intents: List[(NCIdlIntent/*Intent*/, NCIntentMatch => NCRe
                     convTokToFix.getMetadata.put(s"nlpcraft:limit:indexes", Collections.singletonList(newRef.getIndex))
                 }
 
+            case "nlpcraft:function" =>
+                val refId = convTokToFix.meta[String]("nlpcraft:function:note")
+                val refIdxs = convTokToFix.meta[JList[Int]]("nlpcraft:function:indexes").asScala
+
+                require(refIdxs.size == 1)
+
+                val refIdx = refIdxs.head
+
+                if (!vrntNotConvToks.exists(isReference(_, refId, refIdx))) {
+                    val newRef = getNewReferences(refId, Seq(refIdx), _.size == 1).head
+
+                    convTokToFix.getMetadata.put(s"nlpcraft:function:note", newRef.getId)
+                    convTokToFix.getMetadata.put(s"nlpcraft:function:indexes", Collections.singletonList(newRef.getIndex))
+                }
+
             case "nlpcraft:relation" =>
                 val refId = convTokToFix.meta[String]("nlpcraft:relation:note")
                 val refIdxs = convTokToFix.meta[JList[Int]]("nlpcraft:relation:indexes").asScala.sorted
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/cmdline/NCCliBase.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/cmdline/NCCliBase.scala
index 381a663..378a27f 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/cmdline/NCCliBase.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/cmdline/NCCliBase.scala
@@ -65,7 +65,7 @@ class NCCliBase extends App {
     // | MAKE SURE TO UPDATE THIS VAR WHEN NUMBER OF SERVICES IS CHANGED. |
     // +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^+
     final val NUM_SRV_SERVICES = 31 /*services*/ + 1 /*progress start*/
-    final val NUM_PRB_SERVICES = 24 /*services*/ + 1 /*progress start*/
+    final val NUM_PRB_SERVICES = 25 /*services*/ + 1 /*progress start*/
     // +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^+
     // | MAKE SURE TO UPDATE THIS VAR WHEN NUMBER OF SERVICES IS CHANGED. |
     // +==================================================================+
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/NCProbeBoot.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/NCProbeBoot.scala
index 561860f..dd811fc 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/NCProbeBoot.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/NCProbeBoot.scala
@@ -42,6 +42,7 @@ import org.apache.nlpcraft.probe.mgrs.lifecycle.NCLifecycleManager
 import org.apache.nlpcraft.probe.mgrs.model.NCModelManager
 import org.apache.nlpcraft.probe.mgrs.nlp.NCProbeEnrichmentManager
 import org.apache.nlpcraft.probe.mgrs.nlp.enrichers.dictionary.NCDictionaryEnricher
+import org.apache.nlpcraft.probe.mgrs.nlp.enrichers.function.NCFunctionEnricher
 import org.apache.nlpcraft.probe.mgrs.nlp.enrichers.limit.NCLimitEnricher
 import org.apache.nlpcraft.probe.mgrs.nlp.enrichers.model.NCModelEnricher
 import org.apache.nlpcraft.probe.mgrs.nlp.enrichers.relation.NCRelationEnricher
@@ -518,6 +519,7 @@ private [probe] object NCProbeBoot extends LazyLogging with NCOpenCensusTrace {
             startedMgrs += NCStopWordEnricher.start(span)
             startedMgrs += NCModelEnricher.start(span)
             startedMgrs += NCLimitEnricher.start(span)
+            startedMgrs += NCFunctionEnricher.start(span)
             startedMgrs += NCSortEnricher.start(span)
             startedMgrs += NCRelationEnricher.start(span)
             startedMgrs += NCSuspiciousNounsEnricher.start(span)
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCProbeVariants.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCProbeVariants.scala
index 2b91128..ddcaa19 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCProbeVariants.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCProbeVariants.scala
@@ -52,7 +52,7 @@ object NCProbeVariants {
             )
 
         t.getId match {
-            case "nlpcraft:relation" | "nlpcraft:limit" => meta += "nlpcraft:relation:indexes" -> IDXS
+            case "nlpcraft:relation" | "nlpcraft:limit" | "nlpcraft:function" => meta += s"${t.getId}:indexes" -> IDXS
             case "nlpcraft:sort" => meta += "nlpcraft:sort:subjindexes" -> IDXS2; meta += "nlpcraft:sort:byindexes" -> IDXS2
             case _ => // No-op.
         }
@@ -95,7 +95,7 @@ object NCProbeVariants {
                                 val ps = mkNlpNoteParams()
 
                                 delNote.noteType match {
-                                    case "nlpcraft:relation" | "nlpcraft:limit" => ps += "indexes" -> IDXS
+                                    case "nlpcraft:relation" | "nlpcraft:limit" | "nlpcraft:function" => ps += "indexes" -> IDXS
                                     case "nlpcraft:sort" => ps += "subjindexes" -> IDXS2; ps += "byindexes" -> IDXS2
                                     case _ => // No-op.
                                 }
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCTokenPartKey.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCTokenPartKey.scala
index c89cae1..6fb9ca7 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCTokenPartKey.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCTokenPartKey.scala
@@ -56,6 +56,11 @@ object NCTokenPartKey {
                             "limit" -> part.meta[Double](s"$id:limit"),
                             "note" -> part.meta[String](s"$id:note")
                         )
+                    case "nlpcraft:function" =>
+                        Map(
+                            "type" -> part.meta[String](s"$id:type"),
+                            "note" -> part.meta[String](s"$id:note")
+                        )
                     case "nlpcraft:sort" =>
                         val m = mutable.HashMap.empty[String, Any]
 
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCDeployManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCDeployManager.scala
index 332dd26..1be7644 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCDeployManager.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCDeployManager.scala
@@ -1060,11 +1060,11 @@ object NCDeployManager extends NCService {
         mdl.getElements.asScala.foreach(e => checkMandatoryString(e.getId,"element.id", MODEL_ELEMENT_ID_MAXLEN))
 
         for ((elm, restrs: util.Set[String]) <- mdl.getRestrictedCombinations.asScala) {
-            if (elm != "nlpcraft:limit" && elm != "nlpcraft:sort" && elm != "nlpcraft:relation")
+            if (elm != "nlpcraft:limit" && elm != "nlpcraft:sort" && elm != "nlpcraft:relation" && elm != "nlpcraft:function")
                 throw new NCE(s"Unsupported restricting element [" +
                     s"mdlId=$mdlId, " +
                     s"elmId=$elm" +
-                s"]. Only 'nlpcraft:limit', 'nlpcraft:sort', and 'nlpcraft:relation' are allowed.")
+                s"]. Only 'nlpcraft:limit', 'nlpcraft:sort', 'nlpcraft:function' and 'nlpcraft:relation' are allowed.")
             if (restrs.contains(elm))
                 throw new NCE(s"Element cannot be restricted to itself [" +
                     s"mdlId=$mdlId, " +
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/nlp/NCProbeEnrichmentManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/nlp/NCProbeEnrichmentManager.scala
index 10b2cf7..381f2c9 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/nlp/NCProbeEnrichmentManager.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/nlp/NCProbeEnrichmentManager.scala
@@ -35,6 +35,7 @@ import org.apache.nlpcraft.probe.mgrs.conversation.NCConversationManager
 import org.apache.nlpcraft.probe.mgrs.dialogflow.NCDialogFlowManager
 import org.apache.nlpcraft.probe.mgrs.model.NCModelManager
 import org.apache.nlpcraft.probe.mgrs.nlp.enrichers.dictionary.NCDictionaryEnricher
+import org.apache.nlpcraft.probe.mgrs.nlp.enrichers.function.NCFunctionEnricher
 import org.apache.nlpcraft.probe.mgrs.nlp.enrichers.limit.NCLimitEnricher
 import org.apache.nlpcraft.probe.mgrs.nlp.enrichers.model.NCModelEnricher
 import org.apache.nlpcraft.probe.mgrs.nlp.enrichers.relation.NCRelationEnricher
@@ -433,6 +434,7 @@ object NCProbeEnrichmentManager extends NCService with NCOpenCensusModelStats {
                 Some(Holder(NCModelEnricher, () => nlpSen.flatten.filter(_.isUser))),
                 get("nlpcraft:sort", NCSortEnricher),
                 get("nlpcraft:limit", NCLimitEnricher),
+                get("nlpcraft:function", NCFunctionEnricher),
                 get("nlpcraft:relation", NCRelationEnricher)
             ).flatten
 
@@ -483,6 +485,7 @@ object NCProbeEnrichmentManager extends NCService with NCOpenCensusModelStats {
                         h.enricher match {
                             case NCSortEnricher => same = squeeze("nlpcraft:sort")
                             case NCLimitEnricher => same = squeeze("nlpcraft:limit")
+                            case NCFunctionEnricher => same = squeeze("nlpcraft:function")
                             case NCRelationEnricher => same = squeeze("nlpcraft:relation")
 
                             case _ => // No-op.
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/function/NCFunctionEnricher.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/function/NCFunctionEnricher.scala
new file mode 100644
index 0000000..d68c2ac
--- /dev/null
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/function/NCFunctionEnricher.scala
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nlpcraft.probe.mgrs.nlp.enrichers.function
+
+import io.opencensus.trace.Span
+import org.apache.nlpcraft.common.NCService
+import org.apache.nlpcraft.common.nlp.core.NCNlpCoreManager
+import org.apache.nlpcraft.common.nlp.{NCNlpSentence, NCNlpSentenceNote, NCNlpSentenceToken}
+import org.apache.nlpcraft.probe.mgrs.NCProbeModel
+import org.apache.nlpcraft.probe.mgrs.nlp.NCProbeEnricher
+
+import java.util.Collections
+import scala.jdk.CollectionConverters.{MapHasAsScala, SetHasAsScala}
+
+/**
+  *
+  */
+object NCFunctionEnricher extends NCProbeEnricher {
+    private final val TOK_ID = "nlpcraft:function"
+
+    private case class SingeFunc(name: String, synonyms: Seq[String])
+
+    private object SingeFunc {
+        def apply(name: String, syns:String*): SingeFunc = SingeFunc(name, syns)
+    }
+
+    private final val FUNC_NUM_SINGLE =
+        Set(
+            SingeFunc("sin", "sine"),
+            SingeFunc("cos", "cosine"),
+            SingeFunc("tan", "tangent"),
+            SingeFunc("cot", "cotangent"),
+            SingeFunc("round"),
+            SingeFunc("floor"),
+            SingeFunc("max", "maximum"),
+            SingeFunc("min", "minimum"),
+            SingeFunc("avg", "average"),
+            SingeFunc("sum", "summary")
+        )
+
+    @volatile private var funcNumSingleData: Map[String, String] = _
+
+    override def start(parent: Span = null): NCService = startScopedSpan("start", parent) { _ =>
+        ackStarting()
+
+        funcNumSingleData =
+            FUNC_NUM_SINGLE.flatMap(p => (p.synonyms :+ p.name).toSet.map(NCNlpCoreManager.stem).map(_ -> p.name).toMap).toMap
+
+        ackStarted()
+    }
+
+    /**
+      *
+      * @param parent Optional parent span.
+      */
+    override def stop(parent: Span = null): Unit = startScopedSpan("stop", parent) { _ =>
+        ackStopping()
+
+        funcNumSingleData = null
+
+        ackStopped()
+    }
+
+    override def enrich(mdl: NCProbeModel, ns: NCNlpSentence, senMeta: Map[String, Serializable], parent: Span): Unit = {
+        require(isStarted)
+
+        val restricted =
+            mdl.model.getRestrictedCombinations.asScala.getOrElse(TOK_ID, java.util.Collections.emptySet()).
+                asScala
+
+        startScopedSpan(
+            "enrich", parent, "srvReqId" -> ns.srvReqId, "mdlId" -> mdl.model.getId, "txt" -> ns.text
+        ) { _ =>
+            val buf = collection.mutable.ArrayBuffer.empty[Seq[NCNlpSentenceToken]]
+
+            for (toks <- ns.tokenMixWithStopWords() if toks.size > 1 && !buf.exists(_.containsSlice(toks))) {
+                funcNumSingleData.get(toks.head.stem) match {
+                    case Some(f) =>
+                        val users = toks.tail.filter(_.isUser)
+
+                        if (users.size == 1 && toks.tail.forall(t => users.contains(t) || t.isStopWord)) {
+                            for (typ <- users.head.filter(_.isUser).map(_.noteType) if !restricted.contains(typ))
+                                toks.head.add(
+                                    NCNlpSentenceNote(
+                                        Seq(toks.head.index),
+                                        TOK_ID,
+                                        "type" -> f,
+                                        "indexes" -> Collections.singleton(users.head.index),
+                                        "note" -> typ
+                                    )
+                                )
+                        }
+
+                    case None => // No-op.
+                }
+            }
+        }
+    }
+}
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/sentence/NCSentenceManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/sentence/NCSentenceManager.scala
index 50137a2..daf6796 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/sentence/NCSentenceManager.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/sentence/NCSentenceManager.scala
@@ -46,7 +46,7 @@ object NCSentenceManager extends NCService {
     def getLinks(notes: Seq[NCNlpSentenceNote]): Seq[NoteLink] = {
         val noteLinks = mutable.ArrayBuffer.empty[NoteLink]
 
-        for (n <- notes.filter(n => n.noteType == "nlpcraft:limit" || n.noteType == "nlpcraft:references"))
+        for (n <- notes.filter(n => n.noteType == "nlpcraft:limit" || n.noteType == "nlpcraft:references" || n.noteType == "nlpcraft:function"))
             noteLinks += NoteLink(n("note").asInstanceOf[String], n("indexes").asInstanceOf[JList[Int]].asScala.toSeq.sorted)
 
         for (n <- notes.filter(_.noteType == "nlpcraft:sort")) {
@@ -516,6 +516,7 @@ object NCSentenceManager extends NCService {
 
         fixNoteIndexes("nlpcraft:relation", "indexes", "note", ns)
         fixNoteIndexes("nlpcraft:limit", "indexes", "note", ns)
+        fixNoteIndexes("nlpcraft:function", "indexes", "note", ns)
         fixNoteIndexesList("nlpcraft:sort", "subjindexes", "subjnotes", ns)
         fixNoteIndexesList("nlpcraft:sort", "byindexes", "bynotes", ns)
 
@@ -527,6 +528,7 @@ object NCSentenceManager extends NCService {
         val res =
             fixIndexesReferences("nlpcraft:relation", "indexes", "note", ns, histSeq) &&
             fixIndexesReferences("nlpcraft:limit", "indexes", "note", ns, histSeq) &&
+            fixIndexesReferences("nlpcraft:function", "indexes", "note", ns, histSeq) &&
             fixIndexesReferencesList("nlpcraft:sort", "subjindexes", "subjnotes", ns, histSeq) &&
             fixIndexesReferencesList("nlpcraft:sort", "byindexes", "bynotes", ns, histSeq)
 
@@ -748,7 +750,7 @@ object NCSentenceManager extends NCService {
         addDeleted(sen, sen, swallowed)
         swallowed.foreach(sen.removeNote)
 
-        var sens = mkVariants( sen, mdl, lastPhase, overlappedNotes)
+        var sens = mkVariants(sen, mdl, lastPhase, overlappedNotes)
 
         sens.par.foreach(sen =>
             sen.foreach(tok =>
diff --git a/nlpcraft/src/test/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/NCEnrichersTestBeans.scala b/nlpcraft/src/test/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/NCEnrichersTestBeans.scala
index b4d2f71..5dd797d 100644
--- a/nlpcraft/src/test/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/NCEnrichersTestBeans.scala
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/NCEnrichersTestBeans.scala
@@ -248,6 +248,26 @@ case class NCTestRelationToken(text: String, `type`: String, indexes: Seq[Int],
             s", note=$note>"
 }
 
+case class NCTestFunctionToken(text: String, `type`: String, indexes: Seq[Int], note: String) extends NCTestToken {
+    require(text != null)
+    require(`type` != null)
+    require(indexes != null)
+    require(indexes.nonEmpty)
+    require(note != null)
+
+    override def id: String = "nlpcraft:function"
+    override def toString: String =
+        s"$text(function)" +
+            s"<type=${`type`}" +
+            s", indexes=[${indexes.mkString(",")}]" +
+            s", note=$note>"
+}
+
+object NCTestFunctionToken {
+    def apply(text: String, `type`: String, index: Int, note: String):NCTestFunctionToken =
+        NCTestFunctionToken(text, `type`, Seq(index), note)
+}
+
 case class NCTestLimitToken(
     text: String,
     limit: Double,
@@ -352,7 +372,15 @@ object NCTestToken {
                     indexes = indexes.asScala.toSeq,
                     note = t.meta("nlpcraft:relation:note")
                 )
+            case "nlpcraft:function" =>
+                val indexes: JList[Int] = t.meta("nlpcraft:function:indexes")
 
+                NCTestFunctionToken(
+                    txt,
+                    `type` = t.meta("nlpcraft:function:type"),
+                    indexes = indexes.asScala.toSeq,
+                    note = t.meta("nlpcraft:function:note")
+                )
             case "nlpcraft:limit" =>
                 val indexes: JList[Int] = t.meta("nlpcraft:limit:indexes")
                 val asc: Optional[Boolean] = t.metaOpt("nlpcraft:limit:asc")
diff --git a/nlpcraft/src/test/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/function/NCEnricherFunctionSpec.scala b/nlpcraft/src/test/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/function/NCEnricherFunctionSpec.scala
new file mode 100644
index 0000000..6cc1929
--- /dev/null
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/function/NCEnricherFunctionSpec.scala
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nlpcraft.probe.mgrs.nlp.enrichers.function
+
+import org.apache.nlpcraft.NCTestEnvironment
+import org.apache.nlpcraft.probe.mgrs.nlp.enrichers.{NCDefaultTestModel, NCEnricherBaseSpec, NCTestNlpToken => nlp, NCTestFunctionToken => fun, NCTestUserToken => usr}
+import org.junit.jupiter.api.Test
+
+/**
+  * Limit enricher test.
+  */
+@NCTestEnvironment(model = classOf[NCDefaultTestModel], startClient = true)
+class NCEnricherFunctionSpec extends NCEnricherBaseSpec {
+    /**
+      *
+      * @throws Exception
+      */
+    @Test
+    def test(): Unit =
+        runBatch(
+            _ => checkAll(
+                "max A test",
+                Seq(
+                    fun(text = "max", `type` = "max", index = 1, note = "A"),
+                    usr(text = "A", id = "A"),
+                    nlp(text = "test")
+                )
+            ),
+            _ => checkAll(
+                "maximum the A, maximum the the A",
+                Seq(
+                    fun(text = "maximum", `type` = "max", index = 2, note = "A"),
+                    nlp(text = "the", isStop = true),
+                    usr(text = "A", id = "A"),
+                    nlp(text = ",", isStop = true),
+                    fun(text = "maximum", `type` = "max", index = 6, note = "A"),
+                    nlp(text = "the the", isStop = true),
+                    usr(text = "A", id = "A")
+                )
+            ),
+            _ => checkAll(
+                "maximum the A, maximum the the A the A",
+                Seq(
+                    fun(text = "maximum", `type` = "max", index = 2, note = "A"),
+                    nlp(text = "the", isStop = true),
+                    usr(text = "A", id = "A"),
+                    nlp(text = ",", isStop = true),
+                    fun(text = "maximum", `type` = "max", index = 6, note = "A"),
+                    nlp(text = "the the", isStop = true),
+                    usr(text = "A", id = "A"),
+                    nlp(text = "the", isStop = true),
+                    usr(text = "A", id = "A")
+                )
+            )
+
+        )
+}