This is an automated email from the ASF dual-hosted git repository. sergeykamov pushed a commit to branch NLPCRAFT-491 in repository https://gitbox.apache.org/repos/asf/incubator-nlpcraft.git
commit 5d37fdb2582af5b2e2c78d53714f0c860796d86c Author: Sergey Kamov <[email protected]> AuthorDate: Fri Apr 1 16:42:13 2022 +0300 WIP. --- nlpcraft-examples/order/README.md | 46 +++++++ nlpcraft-examples/order/pom.xml | 62 +++++++++ .../org/apache/nlpcraft/examples/order/Order.scala | 82 ++++++++++++ .../nlpcraft/examples/order/OrderModel.scala | 144 +++++++++++++++++++++ .../order/src/main/resources/order_model.yaml | 60 +++++++++ .../examples/order/NCModelValidationSpec.scala | 30 +++++ .../nlpcraft/examples/order/OrderModelSpec.scala | 54 ++++++++ pom.xml | 1 + 8 files changed, 479 insertions(+) diff --git a/nlpcraft-examples/order/README.md b/nlpcraft-examples/order/README.md new file mode 100644 index 0000000..cf21b29 --- /dev/null +++ b/nlpcraft-examples/order/README.md @@ -0,0 +1,46 @@ +<!-- + 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 + + http://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. +--> + +<img alt="" src="https://nlpcraft.apache.org/images/nlpcraft_logo_black.gif" height="80px"> +<br> + +[](https://raw.githubusercontent.com/apache/opennlp/master/LICENSE) +[](https://github.com/apache/incubator-nlpcraft/actions) +[](https://nlpcraft.apache.org/docs.html) +[](https://gitter.im/apache-nlpcraft/community) + +### Light Switch Example +TODO: +### Documentation +TODO: + +For any questions, feedback or suggestions: + + * View & run other [examples](https://github.com/apache/incubator-nlpcraft/tree/master/nlpcraft-examples) + * Read [documentation](https://nlpcraft.apache.org/docs.html), latest [Javadoc](https://nlpcraft.apache.org/apis/latest/index.html) and [REST APIs](https://nlpcraft.apache.org/using-rest.html) + * Download & Maven/Grape/Gradle/SBT [instructions](https://nlpcraft.apache.org/download.html) + * File a bug or improvement in [JIRA](https://issues.apache.org/jira/projects/NLPCRAFT) + * Post a question at [Stack Overflow](https://stackoverflow.com/questions/ask) using <code>nlpcraft</code> tag + * Access [GitHub](https://github.com/apache/incubator-nlpcraft) mirror repository. + * Join project developers on [[email protected]](mailto:[email protected]) + +### Copyright +Copyright (C) 2021 Apache Software Foundation + +<img src="https://www.apache.org/img/ASF20thAnniversary.jpg" height="64px" alt="ASF Logo"> + + diff --git a/nlpcraft-examples/order/pom.xml b/nlpcraft-examples/order/pom.xml new file mode 100644 index 0000000..41a4266 --- /dev/null +++ b/nlpcraft-examples/order/pom.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + 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 + + http://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. +--> + +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <name>NLPCraft Example Order</name> + <artifactId>nlpcraft-example-order</artifactId> + + <parent> + <artifactId>nlpcraft-parent</artifactId> + <groupId>org.apache.nlpcraft</groupId> + <version>1.0.0</version> + <relativePath>../../pom.xml</relativePath> + </parent> + + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>nlpcraft</artifactId> + <version>${project.version}</version> + </dependency> + + <!-- Test dependencies. --> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>${maven.compiler.plugin.ver}</version> + <configuration> + <source>${java.ver}</source> + <target>${java.ver}</target> + </configuration> + </plugin> + </plugins> + </build> +</project> \ No newline at end of file diff --git a/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/Order.scala b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/Order.scala new file mode 100644 index 0000000..769e8ab --- /dev/null +++ b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/Order.scala @@ -0,0 +1,82 @@ +/* + * 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 + * + * http://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.examples.order + +import scala.collection.mutable + +private enum PizzaSize: + case SMALL, MEDIUM, LARGE + +private class Pizza(name: String, var sizeOpt: Option[PizzaSize]): + override def toString: String = + val sz = Option.when(sizeOpt.nonEmpty)(sizeOpt.toString.toLowerCase).getOrElse("unknown") + s"$name $sz size" + +object Order: + def getPizzaSizeKinds: String = PizzaSize.values.map(_.toString.toLowerCase).mkString(", ") + def pizza2Str(name: String, size: PizzaSize): String = s"$name ${size.toString.toLowerCase} size" + +import Order.* + +/** + * Contract. + * 1. 'mkSpecifyRequest' scans ordered data, finds first invalid element and asks to specify it. + * 2. 'specify' methods (specifyPizzaSize) should specify elements in the same order. + * So, we don't need to save which concrete element we treying to specify. + */ +class Order: + private val pizza = mutable.LinkedHashMap.empty[String, PizzaSize] + private val drinks = mutable.LinkedHashSet.empty[String] + + def addDrink(drink: String): Unit = drinks += drink + def addPizza(name: String, size: PizzaSize): Unit = pizza += name -> size + def addPizza(name: String): Unit = pizza += name -> null + + def inProgress(): Boolean = pizza.nonEmpty || drinks.nonEmpty + + def isValid(): Boolean = + (pizza.nonEmpty || drinks.nonEmpty) && + (pizza.isEmpty || pizza.forall{ (_, size) => size != null } ) + + def specifyPizzaSize(sz: PizzaSize): Boolean = pizza.find { (_, size) => size == null } match + case Some((name, _)) => pizza += name -> sz; true + case None => false + + def ask2Specify(): String = + require(!isValid()) + + if pizza.isEmpty && drinks.isEmpty then + "Order is empty. Please order some pizza or drinks." + else + pizza.find { (_, size) => size == null } match + case Some((name, _)) => s"Please specify $name size? It can be $getPizzaSizeKinds" + case None => throw new AssertionError("Invalid state") + + def ask2Confirm(): String = + require(isValid()) + s"Let me specify your order.\n${this.toString()}\nIs it correct?" + + override def toString(): String = + val ps = if pizza.nonEmpty then s"Pizza: ${pizza.map { (name, sz) => pizza2Str(name, sz) }.mkString(", ")}. " else "" + val ds = if drinks.nonEmpty then s"Drinks: ${drinks.mkString(", ")}. " else "" + + s"$ps$ds" + + def clear(): Unit = + pizza.clear() + drinks.clear() \ No newline at end of file diff --git a/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/OrderModel.scala b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/OrderModel.scala new file mode 100644 index 0000000..7f0bc74 --- /dev/null +++ b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/OrderModel.scala @@ -0,0 +1,144 @@ +/* + * 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 + * + * http://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.examples.order + +import org.apache.nlpcraft.* +import org.apache.nlpcraft.internal.util.NCResourceReader +import org.apache.nlpcraft.nlp.* +import org.apache.nlpcraft.nlp.entity.parser.semantic.NCSemanticEntityParser +import org.apache.nlpcraft.nlp.entity.parser.* +import org.apache.nlpcraft.nlp.token.enricher.NCEnStopWordsTokenEnricher +import org.apache.nlpcraft.nlp.token.parser.NCOpenNLPTokenParser +import scala.collection.mutable +import org.apache.nlpcraft.NCResultType.* +import scala.jdk.CollectionConverters.* + +/** + * This example provides very simple implementation for NLI-powered light switch. + * You can say something like this: + * <ul> + * <li>"Turn the lights off in the entire house."</li> + * <li>"Switch on the illumination in the master bedroom closet."</li> + * </ul> + * You can easily modify intent callbacks to perform the actual light switching using + * HomeKit or Arduino-based controllers. + * <p> + * See 'README.md' file in the same folder for running and testing instructions. + */ + +class OrderModel extends NCModelAdapter( + new NCModelConfig("nlpcraft.order.ex", "Order Example Model", "1.0"), + new NCPipelineBuilder().withSemantic("en", "order_model.yaml").build() +): + private val orders = mutable.HashMap.empty[String, Order] + + private def getOrder(im: NCIntentMatch): Order = orders.getOrElseUpdate(im.getContext.getRequest.getUserId, new Order) + private def extractPizza(e: NCEntity): String = e.get[String]("ord:pizza:kind:value") + private def extractPizzaSize(e: NCEntity): String = e.get[String]("ord:pizza:size:value") + private def extractDrink(e: NCEntity): String = e.get[String]("ord:drink:value") + + private def confirmOrSpecify(ord: Order): NCResult = + if ord.isValid() then + NCResult(ord.ask2Confirm(), ASK_DIALOG) + else + NCResult(ord.ask2Specify(), ASK_DIALOG) + + private def getAvgPosition(e: NCEntity): Double = + val toks = e.getTokens.asScala + + (toks.head.getIndex + toks.last.getIndex) / 2.0 + + @NCIntent("intent=confirm term(confirm)={has(ent_groups, 'confirm')}") + def onConfirm(im: NCIntentMatch, @NCIntentTerm("confirm") confirm: NCEntity): NCResult = + val ord = getOrder(im) + + if !ord.inProgress() then throw new NCRejection("No orders in progress") + + if confirm.getId == "ord:confirm:yes" then + if ord.isValid() then + println("Congratulations. Order executed!") + ord.clear() + NCResult("Order executed.", ASK_RESULT) + else + NCResult(ord.ask2Specify(), ASK_DIALOG) + else + ord.clear() + NCResult("Order cleared. We are ready for new orders.", ASK_RESULT) + + @NCIntent( + "intent=order " + + " term(common)={# == 'ord:common'}* " + + " term(pizzaList)={# == 'ord:pizza:kind'}*" + + " term(pizzaSizesList)={# == 'ord:pizza:size'}* " + + " term(drinkList)={# == 'ord:drink'}*" + ) + @NCIntentSample(Array( + "I want to order margherita, marinara and tea" + )) + def onCommonOrder( + im: NCIntentMatch, + @NCIntentTerm("common") common: List[NCEntity], + @NCIntentTerm("pizzaList") pizzas: List[NCEntity], + @NCIntentTerm("pizzaSizesList") pizzaSizes: List[NCEntity], + @NCIntentTerm("drinkList") drinks: List[NCEntity] + ): NCResult = + if pizzas.isEmpty && drinks.isEmpty then throw new NCRejection("Please order some pizza or drinks") + if pizzaSizes.size > pizzas.size then throw new NCRejection("Pizza and their sizes cannot be recognized") + + val ord = getOrder(im) + + case class Size(entity: NCEntity, position: Double) + + val sizes = mutable.ArrayBuffer.empty ++ pizzaSizes.map(p => Size( p,getAvgPosition(p))) + + pizzas.foreach(p => { + sizes.size match + case 0 => ord.addPizza(extractPizza(p)) + case _ => + val avgPos = getAvgPosition(p) + val nextNeighbour = sizes.minBy(p => Math.abs(avgPos - p.position)) + ord.addPizza(extractPizza(p), PizzaSize.valueOf(extractPizzaSize(nextNeighbour.entity).toUpperCase)) + sizes -= nextNeighbour + }) + + for (p <- drinks.map(extractDrink)) ord.addDrink(p) + + confirmOrSpecify(ord) + + @NCIntent( + "intent=specifyPizzaSize " + + " term(common)={# == 'ord:common'}* " + + " term(size)={# == 'ord:pizza:size'} " + + " term(pizza)={# == 'ord:pizza:kind'}?" + ) + def onSpecifyPizzaSize( + im: NCIntentMatch, + @NCIntentTerm("common") common: List[NCEntity], + @NCIntentTerm("size") size: NCEntity, + @NCIntentTerm("pizza") pizzaOpt: Option[NCEntity] + ): NCResult = + val ord = getOrder(im) + require(!ord.isValid()) + + val sz = PizzaSize.valueOf(extractPizzaSize(size).toUpperCase) + + pizzaOpt match + case Some(pizza) => ord.addPizza(extractPizza(pizza), sz) + case None => if !ord.specifyPizzaSize(sz) then throw new NCRejection("What specified?") + + confirmOrSpecify(ord) \ No newline at end of file diff --git a/nlpcraft-examples/order/src/main/resources/order_model.yaml b/nlpcraft-examples/order/src/main/resources/order_model.yaml new file mode 100644 index 0000000..728f875 --- /dev/null +++ b/nlpcraft-examples/order/src/main/resources/order_model.yaml @@ -0,0 +1,60 @@ +# +# 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 +# +# http://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. +# + +elements: + - id: "ord:common" + description: "Common words for order." + synonyms: + - "{pizza|food}" + - "{drink|lemonade}" + - "{buy|order|bring|delivery}" + + - id: "ord:pizza:kind" + description: "Kinds of pizza." + values: + "margherita": [ ] + "marbonara": [ ] + "marinara": [ ] + + - id: "ord:pizza:size" + description: "Size of pizza." + values: + "small": [ "{small|smallest|min|minimal} {size|*}" ] + "medium": [ "{medium|intermediate} {size|*}" ] + "large": [ "{big|large|max|maximum} {size|*}" ] + + - id: "ord:drink" + description: "Kinds of drinks." + values: + "tea": [ ] + "green tea": [ ] + "coffee": [ ] + "cola": [ "{coca|cola|coca cola|cocacola|coca-cola}" ] + + - id: "ord:confirm:yes" + description: "Conformation (yes)." + groups: ["confirm"] + synonyms: + - "{yes|yeah|right|fine|nice|excellent|good}" + - "{you are|*} {correct|right}" + + - id: "ord:confirm:no" + description: "Conformation (no)." + groups: ["confirm"] + synonyms: + - "{no|nope|incorrect|wrong}" + - "{you are|*} {not|are not|aren't } {correct|right}" diff --git a/nlpcraft-examples/order/src/test/java/org/apache/nlpcraft/examples/order/NCModelValidationSpec.scala b/nlpcraft-examples/order/src/test/java/org/apache/nlpcraft/examples/order/NCModelValidationSpec.scala new file mode 100644 index 0000000..5210050 --- /dev/null +++ b/nlpcraft-examples/order/src/test/java/org/apache/nlpcraft/examples/order/NCModelValidationSpec.scala @@ -0,0 +1,30 @@ +/* + * 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 + * + * http://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.examples.order + +import org.apache.nlpcraft.* +import org.junit.jupiter.api.* + +import scala.util.Using + +/** + * JUnit models validation. + */ +class NCModelValidationSpec: + @Test + def test(): Unit = Using.resource(new NCModelClient(new OrderModel())) { _.validateSamples() } \ No newline at end of file diff --git a/nlpcraft-examples/order/src/test/java/org/apache/nlpcraft/examples/order/OrderModelSpec.scala b/nlpcraft-examples/order/src/test/java/org/apache/nlpcraft/examples/order/OrderModelSpec.scala new file mode 100644 index 0000000..bb324c7 --- /dev/null +++ b/nlpcraft-examples/order/src/test/java/org/apache/nlpcraft/examples/order/OrderModelSpec.scala @@ -0,0 +1,54 @@ +/* + * 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 + * + * http://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.examples.order + +import org.apache.nlpcraft.* +import org.apache.nlpcraft.NCResultType.* +import org.junit.jupiter.api.Test + +import scala.util.Using +import scala.collection.mutable +/** + * + */ +class OrderModelSpec: + @Test + def test(): Unit = + val buf = mutable.ArrayBuffer.empty[String] + + def printDialog(): Unit = for (line <- buf) println(line) + + Using.resource(new NCModelClient(new OrderModel)) { client => + def ask(txt: String, expType: NCResultType): Unit = + val resp = client.ask(txt, null, "userId") + + buf += s">> $txt" + buf += s">> ${resp.getBody} (${resp.getType})" + buf += "" + + if expType != resp.getType then + printDialog() + require(false, s"Unexpected type: ${resp.getType}, expected: ${expType}.") + + ask("I want to order margherita medium size, marbonara, marinara and tea", ASK_DIALOG) + ask("large size please", ASK_DIALOG) + ask("smallest", ASK_DIALOG) + ask("you are right", ASK_RESULT) + + printDialog() + } \ No newline at end of file diff --git a/pom.xml b/pom.xml index 669f422..3659f9c 100644 --- a/pom.xml +++ b/pom.xml @@ -393,6 +393,7 @@ <module>nlpcraft-examples/lightswitch</module> <module>nlpcraft-examples/lightswitch-ru</module> <module>nlpcraft-examples/lightswitch-fr</module> + <module>nlpcraft-examples/order</module> </modules> </profile> </profiles>
