add comms
This commit is contained in:
commit
1395669d67
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
node_modules/
|
||||
/out
|
||||
/build
|
||||
build/
|
||||
/.gradle
|
||||
/logs
|
||||
/.idea
|
||||
|
||||
save_flow.xml
|
||||
*.iml
|
||||
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Python virtual environment
|
||||
**/.venv/
|
||||
**/venv/
|
||||
|
||||
# Python cache
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.pyd
|
||||
25
README.md
Normal file
25
README.md
Normal file
@ -0,0 +1,25 @@
|
||||
### Projekt obsahuje Furhat skill určený na podporu vybraných kognitívnych funkcií používateľa.
|
||||
|
||||
## Obsah projektu
|
||||
|
||||
- `src/main/kotlin` – hlavná implementácia Furhat skillu
|
||||
- `src/main/resources` – zdroje používané skillom
|
||||
- `src/main/realtime` – proxy modul pre komunikáciu s OpenAI Realtime API
|
||||
- `assets` – doplnkové súbory aplikácie, ak sa používajú
|
||||
- `skill.properties` – konfiguračný súbor Furhat skillu
|
||||
|
||||
## Spustenie skillu
|
||||
|
||||
1. Otvoriť projekt v IntelliJ IDEA.
|
||||
2. Skontrolovať konfiguráciu Furhat SDK.
|
||||
3. Spustiť skill cez Gradle alebo cez Run konfiguráciu.
|
||||
|
||||
## Spustenie proxy modulu
|
||||
|
||||
1. Vytvoriť `.env` podľa súboru `.env.example`.
|
||||
2. Nainštalovať potrebné Python balíky.
|
||||
3. Spustiť FastAPI server.
|
||||
|
||||
## Poznámka
|
||||
|
||||
Súbory s logmi a súkromné konfiguračné údaje nie sú súčasťou repozitára.
|
||||
7
assets/webTemplates/BASIC/css/bootstrap.min.css
vendored
Normal file
7
assets/webTemplates/BASIC/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
18
assets/webTemplates/BASIC/css/style.css
Normal file
18
assets/webTemplates/BASIC/css/style.css
Normal file
@ -0,0 +1,18 @@
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body{
|
||||
background: linear-gradient(-60deg, #ff5858 0%, #f09819 100%);
|
||||
}
|
||||
|
||||
.center{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p{
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
342
assets/webTemplates/BASIC/dist/bundle.js
vendored
Normal file
342
assets/webTemplates/BASIC/dist/bundle.js
vendored
Normal file
@ -0,0 +1,342 @@
|
||||
/******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId]) {
|
||||
/******/ return installedModules[moduleId].exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = {
|
||||
/******/ i: moduleId,
|
||||
/******/ l: false,
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.l = true;
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/******/
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules;
|
||||
/******/
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules;
|
||||
/******/
|
||||
/******/ // identity function for calling harmony imports with the correct context
|
||||
/******/ __webpack_require__.i = function(value) { return value; };
|
||||
/******/
|
||||
/******/ // define getter function for harmony exports
|
||||
/******/ __webpack_require__.d = function(exports, name, getter) {
|
||||
/******/ if(!__webpack_require__.o(exports, name)) {
|
||||
/******/ Object.defineProperty(exports, name, {
|
||||
/******/ configurable: false,
|
||||
/******/ enumerable: true,
|
||||
/******/ get: getter
|
||||
/******/ });
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||
/******/ __webpack_require__.n = function(module) {
|
||||
/******/ var getter = module && module.__esModule ?
|
||||
/******/ function getDefault() { return module['default']; } :
|
||||
/******/ function getModuleExports() { return module; };
|
||||
/******/ __webpack_require__.d(getter, 'a', getter);
|
||||
/******/ return getter;
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Object.prototype.hasOwnProperty.call
|
||||
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
||||
/******/
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = "/dist/";
|
||||
/******/
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(__webpack_require__.s = 2);
|
||||
/******/ })
|
||||
/************************************************************************/
|
||||
/******/ ([
|
||||
/* 0 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
module.exports = __webpack_require__(3)
|
||||
|
||||
|
||||
/***/ }),
|
||||
/* 1 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
module.exports = __webpack_require__(4)
|
||||
|
||||
|
||||
/***/ }),
|
||||
/* 2 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
|
||||
var _furhatCore = __webpack_require__(0);
|
||||
|
||||
var _furhatCore2 = _interopRequireDefault(_furhatCore);
|
||||
|
||||
var _furhatGui = __webpack_require__(1);
|
||||
|
||||
var _furhatGui2 = _interopRequireDefault(_furhatGui);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
(0, _furhatGui2.default)(function (furhat) {
|
||||
furhat.subscribe('furhatos.event.responses.ResponseSkillGUIName', function (data) {
|
||||
if (data.port == window.location.port) {
|
||||
setPageTitle(data.name);
|
||||
}
|
||||
});
|
||||
|
||||
furhat.subscribe('furhatos.event.actions.ActionSkillGUIClear', function (data) {
|
||||
if (data.port == window.location.port) {
|
||||
clearScreen();
|
||||
}
|
||||
});
|
||||
|
||||
furhat.subscribe('furhatos.event.actions.ActionSkillGUIWrite', function (data) {
|
||||
if (data.port == window.location.port) {
|
||||
clearScreen();
|
||||
appendText(data.text);
|
||||
}
|
||||
});
|
||||
|
||||
furhat.subscribe('furhatos.event.actions.ActionSkillGUIAppend', function (data) {
|
||||
if (data.port == window.location.port) {
|
||||
appendText(data.text);
|
||||
}
|
||||
});
|
||||
|
||||
furhat.send({
|
||||
event_name: 'furhatos.event.requests.RequestSkillGUIName',
|
||||
port: window.location.port
|
||||
});
|
||||
});
|
||||
|
||||
function setPageTitle(title) {
|
||||
document.getElementsByTagName("title")[0].innerText = title;
|
||||
}
|
||||
|
||||
function appendText(text) {
|
||||
var p = document.createElement('p');
|
||||
|
||||
p.innerText = text;
|
||||
document.getElementById('root').appendChild(p);
|
||||
}
|
||||
|
||||
function clearScreen() {
|
||||
var root = document.getElementById('root');
|
||||
|
||||
while (root.firstChild) {
|
||||
root.removeChild(root.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
/***/ }),
|
||||
/* 3 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
// Constants
|
||||
const OPEN = 'open';
|
||||
const CLOSE = 'closed';
|
||||
const FAIL = 'failed';
|
||||
|
||||
/**
|
||||
* Furhat main class. Maintains the websocket connection to furhatOS and
|
||||
* has methods to send events, subscribe to events and helper methods such as say,
|
||||
* gesture, etc.
|
||||
*/
|
||||
class Furhat {
|
||||
constructor() {
|
||||
this.eventFunctions = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the furhat socket connection and executes the callback method.
|
||||
* @param domain IP Address for furhatOS - localhost if SDK.
|
||||
* @param port port for RealTimeAPI module of furhatOS.
|
||||
* @param route route for RealTimeAPI module of furhatOS.
|
||||
* @param callback callback method to be executed on successful opening of websocket.
|
||||
*/
|
||||
init(domain, port, route, callback) {
|
||||
if (this.socket !== undefined) {
|
||||
this.socket.close();
|
||||
}
|
||||
console.log(`initializing ws://${domain}:${port}/${route}`); // eslint-disable-line no-console
|
||||
this.socket = new window.WebSocket(`ws://${domain}:${port}/${route}`); // eslint-disable-line no-undef
|
||||
|
||||
this.socket.onopen = () => {
|
||||
this.status = OPEN;
|
||||
if (callback !== undefined) {
|
||||
callback(OPEN, this);
|
||||
}
|
||||
};
|
||||
this.socket.onmessage = event => {
|
||||
if (this.eventFunctions[JSON.parse(event.data).event_name] !== undefined) {
|
||||
this.eventFunctions[JSON.parse(event.data).event_name](JSON.parse(event.data));
|
||||
}
|
||||
};
|
||||
this.socket.onclose = () => {
|
||||
this.status = CLOSE;
|
||||
if (callback !== undefined) {
|
||||
callback(CLOSE, this);
|
||||
}
|
||||
};
|
||||
this.socket.onerror = () => {
|
||||
this.status = FAIL;
|
||||
if (callback !== undefined) {
|
||||
callback(FAIL, this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an event to furhatOS
|
||||
* @param event Object containing the event. Mandtory to have event_name parameter in the object
|
||||
*/
|
||||
send(event) {
|
||||
if (this.socket.readyState === 2 || this.socket.readyState === 3) {
|
||||
// SHIT
|
||||
} else if (this.socket.readyState === 1) {
|
||||
this.socket.send(JSON.stringify(event));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the given event and triggers the supplied callback on event
|
||||
* @param eventName Name of the event to subscribe
|
||||
* @param callback Function which needs to be triggered when the given event is recieved
|
||||
* @param dontSend [Optional] [false by default] Boolean which determines wether to send
|
||||
* the subscribe event or not. use it to set callbacks for event that are already subscribed to,
|
||||
* for instance with group subscriptions
|
||||
*/
|
||||
subscribe(eventName, callback, dontSend = false) {
|
||||
const event = { event_name: 'furhatos.event.actions.ActionRealTimeAPISubscribe', name: eventName };
|
||||
this.eventFunctions[eventName] = callback;
|
||||
if (!dontSend) {
|
||||
this.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the given event group
|
||||
* @param groupNumber Number(Assigned ENUM) of the group that needs to be subscribed to
|
||||
*/
|
||||
subscribeGroup(groupNumber) {
|
||||
const event = { event_name: 'furhatos.event.actions.ActionRealTimeAPISubscribe', group: groupNumber };
|
||||
this.send(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Says a given text
|
||||
* @param text Text which needs to be said by Furhat
|
||||
*/
|
||||
say(text) {
|
||||
const event = { event_name: 'furhatos.event.actions.ActionSpeech', text };
|
||||
this.send(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stimulates the speech of a user in the interaction space
|
||||
* @param text Text which needs to be said by the user
|
||||
*/
|
||||
userSpeech(text) {
|
||||
const event = { event_name: 'furhatos.event.senses.SenseTypingEnd', messageText: text };
|
||||
this.send(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stimulates SenseSpeechStart event. Can be used to stimulate user speech via typing
|
||||
*/
|
||||
userSpeechStart() {
|
||||
const event = { event_name: 'furhatos.event.senses.SenseTypingStart' };
|
||||
this.send(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the given gesture
|
||||
* @param name Name of the gesture that needs to be performed
|
||||
*/
|
||||
gesture(name) {
|
||||
const event = { event_name: 'furhatos.event.actions.ActionGesture', name };
|
||||
this.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
exports.default = Furhat;
|
||||
|
||||
/***/ }),
|
||||
/* 4 */
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
|
||||
var _furhatCore = __webpack_require__(0);
|
||||
|
||||
var _furhatCore2 = _interopRequireDefault(_furhatCore);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
let portNumber;
|
||||
let callbackFun;
|
||||
|
||||
const InitCallback = (status, hat) => {
|
||||
if (status === 'open') {
|
||||
hat.send({
|
||||
event_name: 'furhatos.event.senses.SenseSkillGUIConnected',
|
||||
port: portNumber
|
||||
});
|
||||
callbackFun(hat);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* FurhatGUI Function which sets up a connection to the furhat skill and gives
|
||||
* the furhat object to send and recieve events to the skill.
|
||||
* @param callback callback that needs to be triggered when a sucessful connection is established
|
||||
*/
|
||||
const FurhatGUI = callback => {
|
||||
if (callback !== undefined && typeof callback === 'function') {
|
||||
window.fetch('/port', { method: 'GET' }).then(r => {
|
||||
// eslint-disable-line no-undef
|
||||
r.json().then(o => {
|
||||
const furhat = new _furhatCore2.default();
|
||||
portNumber = o.port;
|
||||
callbackFun = callback;
|
||||
furhat.init(o.address, o.port, 'api', InitCallback); // eslint-disable-line no-undef
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.default = FurhatGUI;
|
||||
|
||||
/***/ })
|
||||
/******/ ]);
|
||||
21
assets/webTemplates/BASIC/index.html
Normal file
21
assets/webTemplates/BASIC/index.html
Normal file
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/style.css">
|
||||
<title>Furhat Skill</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div id="root" class="center">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="dist/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
70
build.gradle
Normal file
70
build.gradle
Normal file
@ -0,0 +1,70 @@
|
||||
plugins {
|
||||
id 'org.jetbrains.kotlin.jvm' version '1.8.21'
|
||||
id 'com.github.johnrengelman.shadow' version '6.1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
//Defines what version of Java to use.
|
||||
sourceCompatibility = 1.8
|
||||
|
||||
//Defines how Kotlin should compile.
|
||||
compileKotlin {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
|
||||
kotlinOptions {
|
||||
//Defines what jvm bytecode to use, 1.8 rather than 1.6
|
||||
jvmTarget = "1.8"
|
||||
apiVersion = "1.8"
|
||||
languageVersion = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
//Defines how Kotlin should compile when testingTry to keep it the same as compileKotlin.
|
||||
compileTestKotlin {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
|
||||
kotlinOptions {
|
||||
//Defines what jvm bytecode to use, 1.8 rather than 1.6
|
||||
jvmTarget = "1.8"
|
||||
apiVersion = "1.8"
|
||||
languageVersion = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven { url "https://s3-eu-west-1.amazonaws.com/furhat-maven/releases"}
|
||||
maven { url 'https://repo.gradle.org/gradle/libs-releases' }
|
||||
maven { url { "https://repo1.maven.org/maven2/" } }
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation 'com.furhatrobotics.furhatos:furhat-commons:2.9.0'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
|
||||
}
|
||||
|
||||
jar {
|
||||
def lowerCasedName = baseName.toLowerCase()
|
||||
def normalizedName = lowerCasedName.substring(0,1).toUpperCase() + lowerCasedName.substring(1)
|
||||
manifest.attributes(
|
||||
'Class-Path': configurations.compileClasspath.collect { it.getName() }.join(' '),
|
||||
'Main-Class': "furhatos.app.${lowerCasedName}.${normalizedName}Skill"
|
||||
)
|
||||
}
|
||||
|
||||
//ShadowJar depends on jar being finished properly.
|
||||
shadowJar {
|
||||
manifest {
|
||||
exclude '**/Log4j2Plugins.dat'
|
||||
exclude '**/node_modules'
|
||||
}
|
||||
from "skill.properties"
|
||||
from "assets"
|
||||
extension 'skill'
|
||||
}
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.4-bin.zip
|
||||
172
gradlew
vendored
Normal file
172
gradlew
vendored
Normal file
@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
84
gradlew.bat
vendored
Normal file
84
gradlew.bat
vendored
Normal file
@ -0,0 +1,84 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
15
skill.properties
Normal file
15
skill.properties
Normal file
@ -0,0 +1,15 @@
|
||||
name = Blank
|
||||
mainclass = furhatos.app.blank.BlankSkill
|
||||
version = 1.0.0
|
||||
language = en-US
|
||||
logLevel = INFO
|
||||
#You may set this to a Furhat version.
|
||||
requiresVersion = false
|
||||
#Set to true if this skill should fail if there is no camera
|
||||
requiresCamera = false
|
||||
#Set to true if this skill should fail if there is no speaker
|
||||
requiresSpeaker = false
|
||||
#Set to true if this skill should fail if there is no microphone
|
||||
requiresMicrophone = false
|
||||
#Set to true if this skill should fail if there is no active recognizer
|
||||
requiresRecognizer = false
|
||||
26
src/main/kotlin/furhatos/app/blank/flow/init.kt
Normal file
26
src/main/kotlin/furhatos/app/blank/flow/init.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package furhatos.app.blank.flow
|
||||
|
||||
import furhatos.app.blank.flow.main.Idle
|
||||
import furhatos.app.blank.flow.main.supporting.SmileIdle
|
||||
import furhatos.app.blank.setting.DISTANCE_TO_ENGAGE
|
||||
import furhatos.app.blank.setting.MAX_NUMBER_OF_USERS
|
||||
import furhatos.flow.kotlin.State
|
||||
import furhatos.flow.kotlin.furhat
|
||||
import furhatos.flow.kotlin.state
|
||||
import furhatos.flow.kotlin.users
|
||||
import furhatos.util.Gender
|
||||
import furhatos.util.Language
|
||||
|
||||
val Init: State = state {
|
||||
init {
|
||||
/** Set our default interaction parameters */
|
||||
users.setSimpleEngagementPolicy(DISTANCE_TO_ENGAGE, MAX_NUMBER_OF_USERS)
|
||||
furhat.setVoice(Language.SLOVAK, gender = Gender.FEMALE)
|
||||
furhat.gesture(SmileIdle, async = true)
|
||||
}
|
||||
onEntry {
|
||||
|
||||
goto(Idle)
|
||||
}
|
||||
|
||||
}
|
||||
433
src/main/kotlin/furhatos/app/blank/flow/main/SessionLogger.kt
Normal file
433
src/main/kotlin/furhatos/app/blank/flow/main/SessionLogger.kt
Normal file
@ -0,0 +1,433 @@
|
||||
package furhatos.app.blank.flow.main
|
||||
|
||||
import furhatos.app.blank.flow.main.supporting.general.sendLogEvent
|
||||
import furhatos.app.blank.flow.main.supporting.general.sendLogJsonLine
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.util.UUID
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.concurrent.thread
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
object SessionLogger {
|
||||
|
||||
private val logsDir: Path = Paths.get(System.getProperty("user.dir"), "logs")
|
||||
private val csvLogFile: Path = logsDir.resolve("sessions_log.csv")
|
||||
private val pendingLogFile: Path = logsDir.resolve("pending_logs.jsonl")
|
||||
private var sessionId: String = ""
|
||||
|
||||
private val pendingFileLock = Any()
|
||||
private val flushInProgress = AtomicBoolean(false)
|
||||
|
||||
init {
|
||||
if (!Files.exists(logsDir)) {
|
||||
Files.createDirectories(logsDir)
|
||||
}
|
||||
|
||||
if (!Files.exists(csvLogFile)) {
|
||||
Files.write(
|
||||
csvLogFile,
|
||||
"timestamp;session_id;game;phase;question;attempt;result;correct_answer;user_answer;hint_used\n"
|
||||
.toByteArray(Charsets.UTF_8),
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.APPEND
|
||||
)
|
||||
}
|
||||
|
||||
if (!Files.exists(pendingLogFile)) {
|
||||
Files.createFile(pendingLogFile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun newSessionId(): String {
|
||||
return UUID.randomUUID()
|
||||
.toString()
|
||||
.replace("-", "")
|
||||
.take(6)
|
||||
.uppercase()
|
||||
}
|
||||
|
||||
private fun esc(value: String?): String {
|
||||
if (value == null) return ""
|
||||
return value
|
||||
.replace("\"", "\"\"")
|
||||
.replace("\n", " ")
|
||||
.replace("\r", " ")
|
||||
.trim()
|
||||
}
|
||||
|
||||
private fun jsonEscape(value: String?): String {
|
||||
if (value == null) return ""
|
||||
return value
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t")
|
||||
}
|
||||
|
||||
private fun buildCsvLine(
|
||||
ts: String,
|
||||
sessionId: String,
|
||||
game: String,
|
||||
phase: String,
|
||||
question: String,
|
||||
attempt: Int,
|
||||
result: String,
|
||||
correctAnswer: String,
|
||||
userAnswer: String?,
|
||||
hintUsed: Boolean
|
||||
): String {
|
||||
return buildString {
|
||||
append('"').append(esc(ts)).append('"').append(';')
|
||||
append('"').append(esc(sessionId)).append('"').append(';')
|
||||
append('"').append(esc(game)).append('"').append(';')
|
||||
append('"').append(esc(phase)).append('"').append(';')
|
||||
append('"').append(esc(question)).append('"').append(';')
|
||||
append(attempt).append(';')
|
||||
append('"').append(esc(result)).append('"').append(';')
|
||||
append('"').append(esc(correctAnswer)).append('"').append(';')
|
||||
append('"').append(esc(userAnswer)).append('"').append(';')
|
||||
append(hintUsed)
|
||||
append('\n')
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildJsonLine(
|
||||
ts: String,
|
||||
sessionId: String,
|
||||
game: String,
|
||||
phase: String,
|
||||
question: String,
|
||||
attempt: Int,
|
||||
result: String,
|
||||
correctAnswer: String,
|
||||
userAnswer: String?,
|
||||
hintUsed: Boolean
|
||||
): String {
|
||||
return buildString {
|
||||
append("{")
|
||||
append("\"created_at\":\"").append(jsonEscape(ts)).append("\",")
|
||||
append("\"session_id\":\"").append(jsonEscape(sessionId)).append("\",")
|
||||
append("\"game\":\"").append(jsonEscape(game)).append("\",")
|
||||
append("\"phase\":\"").append(jsonEscape(phase)).append("\",")
|
||||
append("\"question\":\"").append(jsonEscape(question)).append("\",")
|
||||
append("\"attempt\":").append(attempt).append(",")
|
||||
append("\"result\":\"").append(jsonEscape(result)).append("\",")
|
||||
|
||||
append("\"user_answer\":")
|
||||
if (userAnswer.isNullOrBlank()) append("null")
|
||||
else append("\"").append(jsonEscape(userAnswer)).append("\"")
|
||||
append(",")
|
||||
|
||||
append("\"correct_answer\":")
|
||||
if (correctAnswer.isBlank()) append("null")
|
||||
else append("\"").append(jsonEscape(correctAnswer)).append("\"")
|
||||
append(",")
|
||||
|
||||
append("\"hint_used\":").append(hintUsed)
|
||||
append("}")
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: all pending-file operations go through one lock
|
||||
private fun readPendingLinesLocked(): List<String> {
|
||||
if (!Files.exists(pendingLogFile)) return emptyList()
|
||||
|
||||
return Files.readAllLines(pendingLogFile, Charsets.UTF_8)
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
}
|
||||
|
||||
// NEW
|
||||
private fun writePendingLinesLocked(lines: List<String>) {
|
||||
val content =
|
||||
if (lines.isEmpty()) ""
|
||||
else lines.joinToString(separator = "\n", postfix = "\n")
|
||||
|
||||
Files.write(
|
||||
pendingLogFile,
|
||||
content.toByteArray(Charsets.UTF_8),
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING
|
||||
)
|
||||
}
|
||||
|
||||
private fun appendPendingJsonLine(jsonLine: String) {
|
||||
synchronized(pendingFileLock) {
|
||||
Files.write(
|
||||
pendingLogFile,
|
||||
(jsonLine + "\n").toByteArray(Charsets.UTF_8),
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.APPEND
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// private fun appendPendingJsonLine(jsonLine: String) {
|
||||
// Files.write(
|
||||
// pendingLogFile,
|
||||
// (jsonLine + "\n").toByteArray(Charsets.UTF_8),
|
||||
// StandardOpenOption.CREATE,
|
||||
// StandardOpenOption.APPEND
|
||||
// )
|
||||
// }
|
||||
|
||||
// @Synchronized
|
||||
// fun flushPendingLogs() {
|
||||
// if (!Files.exists(pendingLogFile)) return
|
||||
//
|
||||
// val lines = Files.readAllLines(pendingLogFile, Charsets.UTF_8)
|
||||
// .map { it.trim() }
|
||||
// .filter { it.isNotBlank() }
|
||||
//
|
||||
// if (lines.isEmpty()) return
|
||||
//
|
||||
// val failedLines = mutableListOf<String>()
|
||||
//
|
||||
// for (line in lines) {
|
||||
// val sent = try {
|
||||
// sendLogJsonLine(line)
|
||||
// } catch (e: Exception) {
|
||||
// println("flushPendingLogs exception: ${e.message}")
|
||||
// false
|
||||
// }
|
||||
//
|
||||
// if (!sent) {
|
||||
// failedLines.add(line)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// val newContent =
|
||||
// if (failedLines.isEmpty()) ""
|
||||
// else failedLines.joinToString(separator = "\n", postfix = "\n")
|
||||
//
|
||||
// Files.write(
|
||||
// pendingLogFile,
|
||||
// newContent.toByteArray(Charsets.UTF_8),
|
||||
// StandardOpenOption.TRUNCATE_EXISTING
|
||||
// )
|
||||
// }
|
||||
|
||||
// NEW: background flush
|
||||
fun flushPendingLogsAsync() {
|
||||
if (!flushInProgress.compareAndSet(false, true)) {
|
||||
println("SessionLogger: flush already in progress, skip")
|
||||
return
|
||||
}
|
||||
|
||||
thread(isDaemon = true, name = "pending-log-flush") {
|
||||
try {
|
||||
// 1) Take current batch and immediately free file for new records
|
||||
val batch = synchronized(pendingFileLock) {
|
||||
val lines = readPendingLinesLocked()
|
||||
if (lines.isNotEmpty()) {
|
||||
writePendingLinesLocked(emptyList())
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
if (batch.isEmpty()) {
|
||||
println("SessionLogger: no pending logs to flush")
|
||||
return@thread
|
||||
}
|
||||
|
||||
// 2) Send outside file lock
|
||||
val failedLines = mutableListOf<String>()
|
||||
|
||||
for (line in batch) {
|
||||
val sent = try {
|
||||
sendLogJsonLine(line)
|
||||
} catch (e: Exception) {
|
||||
println("flushPendingLogsAsync exception: ${e.message}")
|
||||
false
|
||||
}
|
||||
|
||||
if (!sent) {
|
||||
failedLines.add(line)
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Put back only failed old lines + lines added meanwhile
|
||||
synchronized(pendingFileLock) {
|
||||
val freshLines = readPendingLinesLocked()
|
||||
val merged = failedLines + freshLines
|
||||
writePendingLinesLocked(merged)
|
||||
}
|
||||
|
||||
println("SessionLogger: background flush finished, failed=${failedLines.size}")
|
||||
} finally {
|
||||
flushInProgress.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun startNewSession() {
|
||||
sessionId = newSessionId()
|
||||
|
||||
log(
|
||||
game = "system",
|
||||
phase = "session",
|
||||
question = "greeting",
|
||||
attempt = 0,
|
||||
result = "session_started",
|
||||
correctAnswer = "",
|
||||
userAnswer = "",
|
||||
hintUsed = false
|
||||
)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun log(
|
||||
game: String,
|
||||
phase: String,
|
||||
question: String,
|
||||
attempt: Int,
|
||||
result: String,
|
||||
correctAnswer: String = "",
|
||||
userAnswer: String? = "",
|
||||
hintUsed: Boolean = false
|
||||
) {
|
||||
if (sessionId.isBlank()) {
|
||||
sessionId = newSessionId()
|
||||
}
|
||||
|
||||
val ts = OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||
// val ts = OffsetDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||||
|
||||
val csvLine = buildCsvLine(
|
||||
ts = ts,
|
||||
sessionId = sessionId,
|
||||
game = game,
|
||||
phase = phase,
|
||||
question = question,
|
||||
attempt = attempt,
|
||||
result = result,
|
||||
correctAnswer = correctAnswer,
|
||||
userAnswer = userAnswer,
|
||||
hintUsed = hintUsed
|
||||
)
|
||||
|
||||
val jsonLine = buildJsonLine(
|
||||
ts = ts,
|
||||
sessionId = sessionId,
|
||||
game = game,
|
||||
phase = phase,
|
||||
question = question,
|
||||
attempt = attempt,
|
||||
result = result,
|
||||
correctAnswer = correctAnswer,
|
||||
userAnswer = userAnswer,
|
||||
hintUsed = hintUsed
|
||||
)
|
||||
|
||||
// локальный архив
|
||||
if (shouldWriteCsv()) {
|
||||
Files.write(
|
||||
csvLogFile,
|
||||
csvLine.toByteArray(Charsets.UTF_8),
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.APPEND
|
||||
)
|
||||
} else {
|
||||
println("SessionLogger: CSV logging skipped because skill is running on robot")
|
||||
}
|
||||
// Files.write(
|
||||
// csvLogFile,
|
||||
// csvLine.toByteArray(Charsets.UTF_8),
|
||||
// StandardOpenOption.CREATE,
|
||||
// StandardOpenOption.APPEND
|
||||
// )
|
||||
|
||||
// потом текущую
|
||||
val sent = try {
|
||||
sendLogEvent(
|
||||
createdAt = ts,
|
||||
sessionId = sessionId,
|
||||
game = game,
|
||||
phase = phase,
|
||||
question = question,
|
||||
attempt = attempt,
|
||||
result = result,
|
||||
userAnswer = userAnswer?.ifBlank { null },
|
||||
correctAnswer = correctAnswer.ifBlank { null },
|
||||
hintUsed = hintUsed
|
||||
)
|
||||
// sendLogJsonLine(jsonLine)
|
||||
} catch (e: Exception) {
|
||||
println("SessionLogger remote send exception: ${e.message}")
|
||||
false
|
||||
}
|
||||
|
||||
if (!sent) {
|
||||
appendPendingJsonLine(jsonLine)
|
||||
println("SessionLogger: remote logging failed, saved to pending_logs.jsonl")
|
||||
} else {
|
||||
println("SessionLogger: remote logging success")
|
||||
}
|
||||
}
|
||||
|
||||
fun isLocalProjectRun(): Boolean {
|
||||
val userDir = Paths.get(System.getProperty("user.dir"))
|
||||
|
||||
val localMarkers = listOf(
|
||||
".idea",
|
||||
"src",
|
||||
"build.gradle",
|
||||
"settings.gradle",
|
||||
"gradlew",
|
||||
"gradlew.bat"
|
||||
)
|
||||
|
||||
return localMarkers.any { marker ->
|
||||
Files.exists(userDir.resolve(marker))
|
||||
}
|
||||
}
|
||||
|
||||
fun isRunningOnRobot(): Boolean = !isLocalProjectRun()
|
||||
|
||||
fun shouldWriteCsv(): Boolean = isLocalProjectRun()
|
||||
|
||||
@Synchronized
|
||||
fun clearCsvIfRunningOnRobot() {
|
||||
if (!isRunningOnRobot()) return
|
||||
if (!Files.exists(csvLogFile)) return
|
||||
|
||||
Files.write(
|
||||
csvLogFile,
|
||||
"timestamp;session_id;game;phase;question;attempt;result;correct_answer;user_answer;hint_used\n"
|
||||
.toByteArray(Charsets.UTF_8),
|
||||
StandardOpenOption.TRUNCATE_EXISTING
|
||||
)
|
||||
|
||||
println("SessionLogger: CSV cleared because skill is running on robot")
|
||||
}
|
||||
|
||||
fun getSessionId(): String = sessionId
|
||||
}
|
||||
|
||||
//==== test ======
|
||||
fun pendingFileStatusMessage(): String {
|
||||
return try {
|
||||
val file = Paths.get(System.getProperty("user.dir"), "logs", "pending_logs.jsonl")
|
||||
|
||||
if (!Files.exists(file)) {
|
||||
return "Pending súbor neexistuje."
|
||||
}
|
||||
|
||||
val lines = Files.readAllLines(file, Charsets.UTF_8)
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
|
||||
when {
|
||||
lines.isEmpty() -> "Pending súbor je prázdny."
|
||||
else -> "Pending súbor obsahuje ${lines.size} záznamov."
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"Nepodarilo sa skontrolovať pending súbor."
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package furhatos.app.blank.flow.main
|
||||
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.flow.main.attention.AttentionTrainingIntro
|
||||
import furhatos.flow.kotlin.State
|
||||
import furhatos.flow.kotlin.*
|
||||
import furhatos.flow.kotlin.furhat
|
||||
import furhatos.app.blank.flow.main.say_time.TimeTrainingIntro
|
||||
import furhatos.app.blank.flow.main.memory.MemoryTrainingIntro
|
||||
import furhatos.app.blank.flow.main.say_time.resetTimeQuestions
|
||||
import furhatos.app.blank.flow.main.supporting.calm
|
||||
import kotlin.random.Random
|
||||
|
||||
// 1 = TimeTrainingIntro, 2 = MemoryTrainingIntro, 3 = AttentionTrainingIntro
|
||||
|
||||
private var gameBag: MutableList<Int> = mutableListOf()
|
||||
|
||||
private fun nextGameFromBag(): Int {
|
||||
if (gameBag.isEmpty()) {
|
||||
gameBag = mutableListOf(1, 2, 3).also { it.shuffle() }
|
||||
}
|
||||
return gameBag.removeAt(0)
|
||||
}
|
||||
|
||||
// Reset bag for a new user/session
|
||||
fun resetTrainingGameBag() {
|
||||
gameBag.clear()
|
||||
}
|
||||
|
||||
val StartQuestion: State = state(Parent) {
|
||||
onEntry {
|
||||
calm()
|
||||
when (nextGameFromBag()) {
|
||||
1 -> {
|
||||
furhat.say("Poďme zahrať hru na orientáciu v čase.")
|
||||
furhat.say("Budem Vám klásť rôzne otázky a Vašou úlohou bude správne pomenovať aktuálny čas.")
|
||||
delay(1000)
|
||||
resetTimeQuestions()
|
||||
goto(TimeTrainingIntro)
|
||||
}
|
||||
2 -> {
|
||||
// furhat.say("Poviem Vám zoznam slov a Vašou úlohou bude ho zopakovať.")
|
||||
furhat.say("Poďme zahrať hru na pamäť.")
|
||||
delay(1000)
|
||||
resetTimeQuestions()
|
||||
goto(MemoryTrainingIntro)
|
||||
}
|
||||
3 -> {
|
||||
furhat.say("Poďme zahrať hru na pozornosť.")
|
||||
furhat.say("Budem hovoriť slová a pri jednom z nich ma budete musieť zastaviť.")
|
||||
delay(1000)
|
||||
resetTimeQuestions()
|
||||
goto(AttentionTrainingIntro)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
106
src/main/kotlin/furhatos/app/blank/flow/main/attention/AST.kt
Normal file
106
src/main/kotlin/furhatos/app/blank/flow/main/attention/AST.kt
Normal file
@ -0,0 +1,106 @@
|
||||
package furhatos.app.blank.flow.main.attention
|
||||
|
||||
object AttentionSmallTalkPhrases {
|
||||
|
||||
fun questionFor(theme: String, target: String): String {
|
||||
val byTheme = when (theme.lowercase()) {
|
||||
"ovocie" -> listOf(
|
||||
"Povedali ste stop pri slove „$target“. Máte radi ovocie?",
|
||||
"Aké ovocie vám chutí najviac?",
|
||||
"Dávate si ovocie skôr ráno, alebo počas dňa?"
|
||||
)
|
||||
|
||||
"sladké" -> listOf(
|
||||
"Povedali ste stop pri slove „$target“. Máte radi sladké?",
|
||||
"Doprajete si občas niečo sladké, alebo skôr nie?",
|
||||
"Ak by ste si mali vybrať niečo sladké, čo by to bolo?"
|
||||
)
|
||||
|
||||
"zelenina" -> listOf(
|
||||
"„$target“ je zelenina. Máte radi zeleninu?",
|
||||
"Ktorú zeleninu máte najradšej?",
|
||||
"Jedávate zeleninu skôr surovú, alebo varenú?"
|
||||
)
|
||||
|
||||
"jedlo" -> listOf(
|
||||
"Slovo „$target“ patrí medzi jedlo. Máte nejaké jedlo, ktoré máte obzvlášť radi?",
|
||||
"Keď sa povie „$target“, napadne vám naň chuť?",
|
||||
"Varíte si radšej doma, alebo máte radi aj jedálne či reštaurácie?"
|
||||
)
|
||||
|
||||
"bobuľa" -> listOf(
|
||||
"„$target“ patrí medzi bobule. Máte radi bobuľové ovocie?",
|
||||
"Spájate si „$target“ skôr s letom, alebo si ho dávate aj počas roka?",
|
||||
"Máte radšej čerstvé bobule, alebo napríklad v koláči či jogurte?"
|
||||
)
|
||||
|
||||
"nápoje" -> listOf(
|
||||
"„$target“ patrí medzi nápoje. Čo najčastejšie pijete počas dňa?",
|
||||
"Máte radšej teplé nápoje alebo studené?",
|
||||
"Keď si chcete oddýchnuť, aký nápoj vám spraví najväčšiu radosť?"
|
||||
)
|
||||
|
||||
"prísady" -> listOf(
|
||||
"„$target“ patrí medzi prísady. Radi varíte alebo pečiete?",
|
||||
"Používate pri varení skôr jednoduché prísady, alebo radi skúšate nové veci?",
|
||||
"Máte nejakú prísadu, bez ktorej si varenie neviete predstaviť?"
|
||||
)
|
||||
|
||||
"dom" -> listOf(
|
||||
"Slovo „$target“ patrí k téme domov. Máte doma nejaké miesto, kde sa cítite najlepšie?",
|
||||
"Máte radšej, keď je doma ticho, alebo keď je tam „živo“?",
|
||||
"Keď si chcete oddýchnuť doma, čo vám najviac pomáha?"
|
||||
)
|
||||
|
||||
"domáce potreby" -> listOf(
|
||||
"„$target“ patrí medzi domáce potreby. Máte doma veci, ktoré používate každý deň?",
|
||||
"Máte radi poriadok a systém, alebo skôr „prirodzený chaos“?",
|
||||
"Keď niečo v domácnosti chýba, všimnete si to hneď?"
|
||||
)
|
||||
|
||||
"technika" -> listOf(
|
||||
"„$target“ patrí medzi techniku. Používate techniku radi, alebo len keď treba?",
|
||||
"Ktoré zariadenie vám doma najviac uľahčuje život?",
|
||||
"Skôr máte radi jednoduché veci, alebo vás baví skúšať nové technológie?"
|
||||
)
|
||||
|
||||
"príroda" -> listOf(
|
||||
"„$target“ patrí k prírode. Máte radi pobyt vonku?",
|
||||
"Čo vám je v prírode príjemnejšie — les, voda, alebo hory?",
|
||||
"Chodíte radšej na krátke prechádzky, alebo na dlhšie výlety?"
|
||||
)
|
||||
|
||||
"mesto" -> listOf(
|
||||
"„$target“ patrí k téme mesto. Máte radi ruch mesta, alebo skôr pokoj?",
|
||||
"Keď ste v meste, čo je pre vás najdôležitejšie — doprava, služby, alebo atmosféra?",
|
||||
"Máte v meste nejaké obľúbené miesto, kam sa radi vraciate?"
|
||||
)
|
||||
|
||||
"telo" -> listOf(
|
||||
"„$target“ súvisí s telom. Venujete sa počas dňa aspoň trochu pohybu?",
|
||||
"Máte radi, keď je deň aktívny, alebo skôr pokojný?",
|
||||
"Čo vám najviac pomáha cítiť sa dobre — prechádzka, oddych, alebo rutina?"
|
||||
)
|
||||
|
||||
"oblečenie" -> listOf(
|
||||
"„$target“ patrí k oblečeniu. Máte radi pohodlné oblečenie, alebo skôr elegantné?",
|
||||
"Vyberáte si oblečenie podľa počasia, alebo skôr podľa nálady?",
|
||||
"Máte nejakú farbu, ktorú nosíte najradšej?"
|
||||
)
|
||||
|
||||
"druh zábavy" -> listOf(
|
||||
"„$target“ patrí k zábave. Ako najradšej trávite voľný čas?",
|
||||
"Máte radšej spoločnú zábavu s ľuďmi, alebo pokojnejšie aktivity?",
|
||||
"Keď si chcete oddýchnuť, čo vás poteší najviac?"
|
||||
)
|
||||
|
||||
else -> listOf(
|
||||
"Zachytili ste slovo „$target“. Je to téma „$theme“. Máte k tomu nejaký vzťah?",
|
||||
"„$target“ — je to z oblasti „$theme“. Páči sa vám táto téma, alebo skôr nie?",
|
||||
"Keď počujete „$target“, napadne vám niečo príjemné, alebo skôr neutrálne?"
|
||||
)
|
||||
}
|
||||
|
||||
return byTheme.random()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,634 @@
|
||||
package furhatos.app.blank.flow.main.attention
|
||||
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.flow.main.SessionLogger
|
||||
import furhatos.app.blank.flow.main.StartQuestion
|
||||
import furhatos.app.blank.flow.main.handlers.Goodbye
|
||||
import furhatos.app.blank.flow.main.handlers.handleRepeat
|
||||
import furhatos.app.blank.flow.main.handlers.handleStop
|
||||
import furhatos.app.blank.flow.main.supporting.AskToContinue
|
||||
import furhatos.app.blank.flow.main.supporting.general.WordBank
|
||||
import furhatos.app.blank.flow.main.supporting.general.WordEntry
|
||||
import furhatos.app.blank.flow.main.supporting.empathy
|
||||
import furhatos.app.blank.flow.main.supporting.littleSad
|
||||
import furhatos.app.blank.flow.main.supporting.veryHappy
|
||||
import furhatos.app.blank.flow.main.supporting.AskIncreaseDifficulty
|
||||
import furhatos.app.blank.flow.main.memory.WordsChecker
|
||||
import furhatos.app.blank.flow.main.supporting.AskDecreaseDifficulty
|
||||
import furhatos.app.blank.flow.main.supporting.ReadyToTrain
|
||||
import furhatos.app.blank.flow.main.supporting.SmallTalk
|
||||
import furhatos.app.blank.flow.main.supporting.general.SmallTalkContext
|
||||
import furhatos.app.blank.flow.main.supporting.general.TrainingMenuFlags
|
||||
import furhatos.app.blank.flow.main.supporting.general.callProxyRespond
|
||||
import furhatos.app.blank.flow.main.supporting.general.genericSmallTalk
|
||||
import furhatos.app.blank.flow.main.supporting.general.isProxyAvailable
|
||||
import furhatos.app.blank.flow.main.supporting.general.requestSmallTalk
|
||||
import furhatos.app.blank.flow.main.supporting.happyNod
|
||||
import furhatos.app.blank.flow.main.supporting.happyShake
|
||||
import furhatos.app.blank.nlu.base_answer.Repeat
|
||||
import furhatos.app.blank.nlu.base_answer.StopTraining
|
||||
import furhatos.flow.kotlin.*
|
||||
import furhatos.gestures.Gestures
|
||||
import kotlin.random.Random
|
||||
|
||||
// -------------------- parametre --------------------
|
||||
|
||||
// na sledovanie toku otázok
|
||||
private const val MAX_LEVEL = 3
|
||||
private const val QUESTIONS_PER_LEVEL = 2
|
||||
private const val MAX_SMALLTALK_PER_SESSION = 3
|
||||
private const val MAX_SAME_LEVEL_QUESTIONS = 6
|
||||
|
||||
var sameLevelQuestionsDone: Int = 0
|
||||
var difficultyWasIncreased: Boolean = false
|
||||
|
||||
// koľko kôl v aktuálnom bloku z 2 bolo neúspešných
|
||||
var failedQuestions: Int = 0
|
||||
|
||||
// Bol aktuálny blok z 2 kôl začatý po zvýšení náročnosti
|
||||
var currentBlockWasIncreased: Boolean = false
|
||||
|
||||
// príznak výsledku aktuálneho kola pre AfterAttentionResolved
|
||||
private var attentionRoundFailed: Boolean = false
|
||||
|
||||
// pomocne premenne pre SmallTak
|
||||
var attentionSmallTalkUsed = 0
|
||||
var lastAttentionTheme: String = ""
|
||||
var lastAttentionTarget: String = ""
|
||||
|
||||
// dĺžka radu slov rastie s úrovňou
|
||||
private fun lengthForLevel(level: Int, rng: Random = Random.Default): Int = when (level) {
|
||||
1 -> listOf(4, 5).random(rng)
|
||||
2 -> listOf(8, 9).random(rng)
|
||||
else -> listOf(10, 11).random(rng)
|
||||
}
|
||||
|
||||
// okno čakania na „stop“ po každom slove
|
||||
private fun listenWindowForLevel(level: Int): Long = when (level) {
|
||||
1 -> 3800
|
||||
2 -> 3500
|
||||
else -> 3200
|
||||
// 1 -> 1800
|
||||
// 2 -> 1500
|
||||
// else -> 1200
|
||||
}
|
||||
|
||||
// -------------------- stav sedenia --------------------
|
||||
|
||||
var attentionLevel: Int = 1
|
||||
var doneAtLevel: Int = 0
|
||||
|
||||
private var wrongAttempts: Int = 0
|
||||
|
||||
private var targetWord: WordEntry? = null
|
||||
private var targetIndex: Int = -1
|
||||
private var sequence: List<WordEntry> = emptyList()
|
||||
private var currentIndex: Int = 0
|
||||
|
||||
// -------------------- generovanie zoznamu --------------------
|
||||
|
||||
private fun buildNewAttentionRound() {
|
||||
val len = lengthForLevel(attentionLevel)
|
||||
|
||||
// target sa vyberá podľa úrovne
|
||||
val target = WordBank.pickSequence(length = 1, maxDifficulty = attentionLevel).first()
|
||||
|
||||
// vyberáju sa ostatné slovaokrem target
|
||||
val others = mutableListOf<WordEntry>()
|
||||
while (others.size < len - 1) {
|
||||
val cand = WordBank.pickSequence(length = 1, maxDifficulty = attentionLevel).first()
|
||||
if (cand.canonical != target.canonical && others.none { it.canonical == cand.canonical }) {
|
||||
others.add(cand)
|
||||
}
|
||||
}
|
||||
|
||||
// vkladánie target do náhodnej pozície
|
||||
val insertAt = Random.nextInt(0, len)
|
||||
val mixed = others.toMutableList()
|
||||
mixed.add(insertAt, target)
|
||||
|
||||
targetWord = target
|
||||
targetIndex = insertAt
|
||||
sequence = mixed
|
||||
|
||||
currentIndex = 0
|
||||
wrongAttempts = 0
|
||||
}
|
||||
|
||||
// -------------------- helpery --------------------
|
||||
|
||||
private fun userSaidStop(text: String?): Boolean {
|
||||
val t = text ?: return false
|
||||
val tokens = WordsChecker.tokenizeMeaningful(t)
|
||||
return tokens.any { it == "stop" }
|
||||
}
|
||||
|
||||
private fun FlowControlRunner.restartSameRound() {
|
||||
currentIndex = 0
|
||||
// sequence/target остаются те же, чтобы попытка была честной
|
||||
goto(SayNextWord)
|
||||
}
|
||||
|
||||
fun requestAttentionExplainWrong(mistakeType: String): String {
|
||||
val target = targetWord?.canonical ?: ""
|
||||
val position = targetIndex + 1
|
||||
|
||||
if (!isProxyAvailable()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return callProxyRespond(
|
||||
userText = "Používateľ urobil chybu v cvičení pozornosti.",
|
||||
task = "explain_wrong",
|
||||
context = mapOf(
|
||||
"exercise" to "attention",
|
||||
"mistake_type" to mistakeType, // early_stop / missed_target / wrong_word
|
||||
"target_word" to target,
|
||||
"target_position" to position,
|
||||
"theme" to (targetWord?.theme ?: ""),
|
||||
"level" to attentionLevel
|
||||
)
|
||||
) ?: ""
|
||||
}
|
||||
|
||||
//==============================================================
|
||||
// Hint
|
||||
//==============================================================
|
||||
fun requestAttentionHint(mistakeType: String): String {
|
||||
val target = targetWord?.canonical ?: ""
|
||||
|
||||
if (!isProxyAvailable()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return callProxyRespond(
|
||||
userText = "Používateľ potrebuje nápovedu.",
|
||||
task = "hint",
|
||||
context = mapOf(
|
||||
"exercise" to "attention",
|
||||
"mistake_type" to mistakeType, // early_stop / missed_target
|
||||
"target_word" to target,
|
||||
"theme" to (targetWord?.theme ?: ""),
|
||||
"level" to attentionLevel
|
||||
)
|
||||
) ?: ""
|
||||
}
|
||||
|
||||
|
||||
//==============================================================
|
||||
// Small Talk
|
||||
//==============================================================
|
||||
val AttentionThemeSmallTalk: State by lazy {
|
||||
SmallTalk(nextState = AfterAttentionResolved) {
|
||||
AttentionSmallTalkPhrases.questionFor(lastAttentionTheme, lastAttentionTarget)
|
||||
}
|
||||
}
|
||||
|
||||
fun nextStateAfterCorrectAttention(): State {
|
||||
if (attentionSmallTalkUsed >= MAX_SMALLTALK_PER_SESSION) return AfterAttentionResolved
|
||||
|
||||
// šanca dostať dodatočnú otázku
|
||||
val totalPlanned = MAX_LEVEL * QUESTIONS_PER_LEVEL // условные 6 заданий
|
||||
val progress = (attentionLevel - 1) * QUESTIONS_PER_LEVEL + doneAtLevel
|
||||
val denom = (totalPlanned - progress).coerceAtLeast(1)
|
||||
|
||||
val smallTalkLeft = MAX_SMALLTALK_PER_SESSION - attentionSmallTalkUsed
|
||||
val percent = minOf(0.5, smallTalkLeft.toDouble() / denom.toDouble())
|
||||
|
||||
val doSmallTalk = Random.nextDouble() < percent
|
||||
if (!doSmallTalk) return AfterAttentionResolved
|
||||
|
||||
attentionSmallTalkUsed++
|
||||
return AttentionThemeSmallTalk
|
||||
}
|
||||
|
||||
fun buildSmallTalkContextAttention(): SmallTalkContext {
|
||||
return SmallTalkContext(
|
||||
exercise = "attention",
|
||||
topic = lastAttentionTheme,
|
||||
subtopic = "theme_question",
|
||||
targetWord = lastAttentionTarget,
|
||||
responseMode = "open"
|
||||
)
|
||||
}
|
||||
|
||||
fun smallTalkManagerAttention(): State {
|
||||
if (attentionSmallTalkUsed >= MAX_SMALLTALK_PER_SESSION) return AfterAttentionResolved
|
||||
|
||||
val totalPlanned = MAX_LEVEL * QUESTIONS_PER_LEVEL
|
||||
val progress = (attentionLevel - 1) * QUESTIONS_PER_LEVEL + doneAtLevel
|
||||
val denom = (totalPlanned - progress).coerceAtLeast(1)
|
||||
|
||||
val smallTalkLeft = MAX_SMALLTALK_PER_SESSION - attentionSmallTalkUsed
|
||||
val percent = minOf(0.5, smallTalkLeft.toDouble() / denom.toDouble())
|
||||
|
||||
val doSmallTalk = Random.nextDouble() < percent
|
||||
if (!doSmallTalk) return AfterAttentionResolved
|
||||
|
||||
val ctx = buildSmallTalkContextAttention()
|
||||
val proxyQuestion = requestSmallTalk(ctx)
|
||||
|
||||
attentionSmallTalkUsed++
|
||||
|
||||
return if (proxyQuestion.isNotBlank()) {
|
||||
genericSmallTalk(
|
||||
context = ctx,
|
||||
nextState = AfterAttentionResolved,
|
||||
preparedQuestion = proxyQuestion
|
||||
)
|
||||
} else {
|
||||
AttentionThemeSmallTalk
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================
|
||||
// Proces hry
|
||||
//==============================================================
|
||||
|
||||
// Intro
|
||||
val AttentionTrainingIntro: State = state(Parent) {
|
||||
onEntry {
|
||||
attentionLevel = 1
|
||||
doneAtLevel = 0
|
||||
attentionSmallTalkUsed = 0
|
||||
|
||||
sameLevelQuestionsDone = 0
|
||||
difficultyWasIncreased = false
|
||||
|
||||
failedQuestions = 0
|
||||
currentBlockWasIncreased = false
|
||||
attentionRoundFailed = false
|
||||
|
||||
buildNewAttentionRound()
|
||||
goto(AskAttention)
|
||||
}
|
||||
}
|
||||
|
||||
// Inštrukcia a začiatok
|
||||
val AskAttention: State = state(Parent) {
|
||||
onEntry {
|
||||
val target = targetWord ?: run {
|
||||
buildNewAttentionRound()
|
||||
targetWord!!
|
||||
}
|
||||
|
||||
delay(1000)
|
||||
furhat.say("Pripravte sa.")
|
||||
furhat.say("Keď budete počuť slovo „${target.canonical}“, povedzte prosím: stop.")
|
||||
delay(1000)
|
||||
furhat.say("Začíname.")
|
||||
|
||||
SessionLogger.log(
|
||||
game = "attention",
|
||||
phase = "question_shown",
|
||||
question = "LEVEL_${attentionLevel}",
|
||||
attempt = wrongAttempts + 1,
|
||||
result = "asked",
|
||||
correctAnswer = "${target.canonical}@${targetIndex + 1}",
|
||||
userAnswer = "",
|
||||
hintUsed = wrongAttempts > 0
|
||||
)
|
||||
|
||||
|
||||
|
||||
goto(SayNextWord)
|
||||
}
|
||||
}
|
||||
|
||||
// ďalšie iné slovo
|
||||
val SayNextWord: State = state(Parent) {
|
||||
onEntry {
|
||||
if (currentIndex >= sequence.size) {
|
||||
goto(HandleMissedStop)
|
||||
return@onEntry
|
||||
}
|
||||
|
||||
val w = sequence[currentIndex]
|
||||
furhat.say(w.canonical)
|
||||
|
||||
// сразу после слова — короткое окно "молчаливого слушания"
|
||||
goto(ListenForStop)
|
||||
}
|
||||
}
|
||||
|
||||
// počuvanie stop
|
||||
val ListenForStop: State = state(Parent) {
|
||||
onEntry {
|
||||
furhat.param.endSilTimeout = 350
|
||||
furhat.param.noSpeechTimeout = listenWindowForLevel(attentionLevel).toInt()
|
||||
|
||||
furhat.attend(users.current)
|
||||
furhat.listen()
|
||||
}
|
||||
|
||||
onResponse<StopTraining> {
|
||||
if (handleStop(it.intent,
|
||||
"attention",
|
||||
"LEVEL_${attentionLevel}",
|
||||
it.text ?: ""))
|
||||
{
|
||||
TrainingMenuFlags.hasTrainedOnce = true
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
return@onResponse
|
||||
}
|
||||
}
|
||||
|
||||
// onResponse<Repeat>{
|
||||
// if (handleRepeat(
|
||||
// it.intent,
|
||||
// "attention",
|
||||
// "LEVEL_${attentionLevel}",
|
||||
// "${targetWord?.canonical}@${targetIndex + 1}",
|
||||
// it.text ?: "",
|
||||
// true
|
||||
// )) {
|
||||
// return@onResponse
|
||||
// }
|
||||
// }
|
||||
|
||||
onResponse {
|
||||
if (userSaidStop(it.text)) {
|
||||
val isCorrectMoment = (currentIndex == targetIndex)
|
||||
|
||||
if (isCorrectMoment) {
|
||||
SessionLogger.log(
|
||||
game = "attention",
|
||||
phase = "answer",
|
||||
question = "LEVEL_${attentionLevel}",
|
||||
attempt = wrongAttempts + 1,
|
||||
result = "success",
|
||||
correctAnswer = "${targetWord?.canonical}@${targetIndex + 1}",
|
||||
userAnswer = it.text ?: "",
|
||||
hintUsed = wrongAttempts > 0
|
||||
)
|
||||
|
||||
furhat.gesture(Gestures.Nod, async = true)
|
||||
veryHappy()
|
||||
furhat.say("Výborne! Správne ste zareagovali.")
|
||||
delay(1000)
|
||||
|
||||
val t = targetWord!!
|
||||
lastAttentionTheme = t.theme
|
||||
lastAttentionTarget = t.canonical
|
||||
|
||||
attentionRoundFailed = false
|
||||
|
||||
// goto(nextStateAfterCorrectAttention())
|
||||
goto(smallTalkManagerAttention())
|
||||
} else {
|
||||
// 'stop' na inom slove
|
||||
wrongAttempts++
|
||||
|
||||
SessionLogger.log(
|
||||
game = "attention",
|
||||
phase = "answer",
|
||||
question = "LEVEL_${attentionLevel}",
|
||||
attempt = wrongAttempts,
|
||||
result = "early_stop",
|
||||
correctAnswer = "${targetWord?.canonical}@${targetIndex + 1}",
|
||||
userAnswer = it.text ?: "",
|
||||
hintUsed = wrongAttempts >= 2
|
||||
)
|
||||
|
||||
when (wrongAttempts) {
|
||||
1 -> {
|
||||
littleSad()
|
||||
furhat.say("Zastavili ste ma pri inom slove.")
|
||||
|
||||
val hint = requestAttentionHint("early_stop")
|
||||
|
||||
SessionLogger.log(
|
||||
game = "attention",
|
||||
phase = "event",
|
||||
question = "LEVEL_${attentionLevel}",
|
||||
attempt = wrongAttempts,
|
||||
result = "hint_requested_early_stop",
|
||||
correctAnswer = "${targetWord?.canonical}@${targetIndex + 1}",
|
||||
userAnswer = it.text ?: "",
|
||||
hintUsed = true
|
||||
)
|
||||
|
||||
if (hint.isNotBlank()){
|
||||
furhat.say("Trochu pomôžem Vám! Dajte mi chvíľu, prosím")
|
||||
furhat.say(hint)
|
||||
delay(1300)
|
||||
}else{
|
||||
happyShake()
|
||||
furhat.say("Nevadí.")
|
||||
furhat.say("Skúsme to ešte raz. Povedzte stop až keď zaznie správne slovo.")
|
||||
}
|
||||
|
||||
restartSameRound()
|
||||
}
|
||||
2 -> {
|
||||
val explanation = requestAttentionExplainWrong("early_stop")
|
||||
val target = targetWord!!.canonical
|
||||
|
||||
if (explanation.isNotBlank()){
|
||||
furhat.say(explanation)
|
||||
}else{
|
||||
empathy()
|
||||
furhat.say("Pomôžem Vám. Správne slovo je „$target“. Skúsme znova.")
|
||||
}
|
||||
|
||||
restartSameRound()
|
||||
}
|
||||
else -> {
|
||||
goto(FinalWrongAnswer)
|
||||
}
|
||||
}
|
||||
}
|
||||
return@onResponse
|
||||
}
|
||||
|
||||
// používateľ povedal niečo iné — ignorovať
|
||||
currentIndex++
|
||||
goto(SayNextWord)
|
||||
|
||||
}
|
||||
|
||||
onNoResponse {
|
||||
currentIndex++
|
||||
goto(SayNextWord)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- nebolo žiadného stop --------------------
|
||||
|
||||
val HandleMissedStop: State = state(Parent) {
|
||||
onEntry {
|
||||
wrongAttempts++
|
||||
|
||||
SessionLogger.log(
|
||||
game = "attention",
|
||||
phase = "answer",
|
||||
question = "LEVEL_${attentionLevel}",
|
||||
attempt = wrongAttempts,
|
||||
result = "missed_target",
|
||||
correctAnswer = "${targetWord?.canonical}@${targetIndex + 1}",
|
||||
userAnswer = "",
|
||||
hintUsed = wrongAttempts >= 2
|
||||
)
|
||||
|
||||
when (wrongAttempts) {
|
||||
1 -> {
|
||||
littleSad()
|
||||
furhat.say("Zmeškali ste správny moment, keď ste ma mali zastaviť.")
|
||||
|
||||
val hint = requestAttentionHint("missed_target")
|
||||
|
||||
if (hint.isNotBlank()){
|
||||
furhat.say("Trochu pomôžem Vám! Dajte mi chvíľu, prosím")
|
||||
furhat.say(hint)
|
||||
}else{
|
||||
happyShake()
|
||||
furhat.say("Nevadí.")
|
||||
furhat.say("Skúsme to ešte raz. Keď zaznie správne slovo, povedzte stop.")
|
||||
}
|
||||
|
||||
restartSameRound()
|
||||
}
|
||||
2 -> {
|
||||
val explanation = requestAttentionExplainWrong("missed_target")
|
||||
val target = targetWord!!.canonical
|
||||
|
||||
if (explanation.isNotBlank()){
|
||||
furhat.say(explanation)
|
||||
}else{
|
||||
happyNod()
|
||||
furhat.say("Pomôžem Vám. Správne slovo bolo „$target“. Skúsme to znova.")
|
||||
}
|
||||
|
||||
restartSameRound()
|
||||
}
|
||||
else -> goto(FinalWrongAnswer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------- 3. chyba -------------------- */
|
||||
|
||||
val FinalWrongAnswer: State = state(Parent) {
|
||||
onEntry {
|
||||
empathy()
|
||||
|
||||
val target = targetWord!!.canonical
|
||||
val position = targetIndex + 1
|
||||
|
||||
SessionLogger.log(
|
||||
game = "attention",
|
||||
phase = "resolved",
|
||||
question = "LEVEL_${attentionLevel}",
|
||||
attempt = wrongAttempts,
|
||||
result = "final_fail",
|
||||
correctAnswer = "$target@$position",
|
||||
userAnswer = "",
|
||||
hintUsed = true
|
||||
)
|
||||
|
||||
furhat.say("Tentoraz to nevyšlo,")
|
||||
happyShake()
|
||||
furhat.say("ale nič sa nedeje.")
|
||||
furhat.say("Povedať stop ste mali pri slove „$target“, ktoré bolo na pozícii $position .")
|
||||
delay(1000)
|
||||
|
||||
attentionRoundFailed = true
|
||||
|
||||
goto(AfterAttentionResolved)
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------- prechod medzi urovni -------------------- */
|
||||
|
||||
val AfterAttentionResolved: State = state(Parent) {
|
||||
onEntry {
|
||||
doneAtLevel++
|
||||
|
||||
if (attentionRoundFailed) {
|
||||
failedQuestions++
|
||||
}
|
||||
attentionRoundFailed = false
|
||||
|
||||
|
||||
if (!difficultyWasIncreased) {
|
||||
sameLevelQuestionsDone++
|
||||
|
||||
if (sameLevelQuestionsDone >= MAX_SAME_LEVEL_QUESTIONS) {
|
||||
veryHappy()
|
||||
furhat.say("Výborne, dnes už stačí. Ukončíme toto cvičenie pozornosti. Ďakujem Vám!")
|
||||
delay(1000)
|
||||
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
return@onEntry
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (doneAtLevel < QUESTIONS_PER_LEVEL) {
|
||||
buildNewAttentionRound()
|
||||
goto(AskAttention)
|
||||
return@onEntry
|
||||
}
|
||||
|
||||
// max -> koniec
|
||||
if (attentionLevel >= MAX_LEVEL) {
|
||||
veryHappy()
|
||||
furhat.say("Týmto sme ukončili hru pozornosti. Ďakujem Vám!")
|
||||
delay(1000)
|
||||
|
||||
TrainingMenuFlags.allAttentionQuestionsCompleted = true
|
||||
TrainingMenuFlags.hasTrainedOnce = true
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
return@onEntry
|
||||
}
|
||||
|
||||
// pokaračovanie
|
||||
val wantsToContinue = call(AskToContinue()) as Boolean
|
||||
if (!wantsToContinue) {
|
||||
TrainingMenuFlags.hasTrainedOnce = true
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
return@onEntry
|
||||
}
|
||||
|
||||
// 2 otázky neúspešné -> znižiť uroveň
|
||||
if (currentBlockWasIncreased &&
|
||||
failedQuestions >= QUESTIONS_PER_LEVEL &&
|
||||
attentionLevel > 1
|
||||
) {
|
||||
val goBack = call(AskDecreaseDifficulty) as Boolean
|
||||
|
||||
if (goBack) {
|
||||
attentionLevel--
|
||||
furhat.say("Dobre, vrátime sa na ľahšiu úroveň.")
|
||||
} else {
|
||||
furhat.say("Dobre, zostaneme na rovnakej úrovni.")
|
||||
}
|
||||
|
||||
doneAtLevel = 0
|
||||
failedQuestions = 0
|
||||
currentBlockWasIncreased = false
|
||||
|
||||
buildNewAttentionRound()
|
||||
goto(AskAttention)
|
||||
return@onEntry
|
||||
}
|
||||
|
||||
// zväčšiť uroveň
|
||||
val increase = call(AskIncreaseDifficulty) as Boolean
|
||||
if (increase && attentionLevel < MAX_LEVEL) {
|
||||
difficultyWasIncreased = true
|
||||
attentionLevel++
|
||||
currentBlockWasIncreased = true
|
||||
furhat.say("Dobre, zvýšime náročnosť.")
|
||||
} else {
|
||||
currentBlockWasIncreased = false
|
||||
furhat.say("Dobre, zostaneme na rovnakej úrovni.")
|
||||
}
|
||||
|
||||
doneAtLevel = 0
|
||||
failedQuestions = 0
|
||||
|
||||
buildNewAttentionRound()
|
||||
goto(AskAttention)
|
||||
}
|
||||
}
|
||||
119
src/main/kotlin/furhatos/app/blank/flow/main/greeting.kt
Normal file
119
src/main/kotlin/furhatos/app/blank/flow/main/greeting.kt
Normal file
@ -0,0 +1,119 @@
|
||||
package furhatos.app.blank.flow.main
|
||||
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.flow.main.attention.AttentionTrainingIntro
|
||||
import furhatos.app.blank.flow.main.handlers.Goodbye
|
||||
import furhatos.app.blank.flow.main.handlers.handleRepeat
|
||||
import furhatos.app.blank.flow.main.handlers.handleRephrase
|
||||
import furhatos.app.blank.flow.main.handlers.handleStop
|
||||
import furhatos.app.blank.flow.main.memory.MemoryTrainingIntro
|
||||
import furhatos.app.blank.flow.main.say_time.TimeTrainingIntro
|
||||
import furhatos.app.blank.flow.main.supporting.CheckCondition
|
||||
import furhatos.app.blank.flow.main.supporting.happyNod
|
||||
import furhatos.app.blank.flow.main.SessionLogger
|
||||
import furhatos.app.blank.flow.main.memory.currentSequence
|
||||
import furhatos.app.blank.flow.main.memory.memoryLevel
|
||||
import furhatos.app.blank.flow.main.supporting.ReadyToTrain
|
||||
import furhatos.app.blank.flow.main.supporting.general.TrainingMenuFlags
|
||||
import furhatos.app.blank.flow.main.supporting.general.isProxyAvailable
|
||||
import furhatos.app.blank.flow.main.supporting.littleSad
|
||||
import furhatos.app.blank.nlu.base_answer.Repeat
|
||||
import furhatos.app.blank.nlu.base_answer.StopTraining
|
||||
import furhatos.flow.kotlin.State
|
||||
import furhatos.flow.kotlin.onResponse
|
||||
import furhatos.flow.kotlin.state
|
||||
import furhatos.flow.kotlin.*
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
||||
private const val PROXY_BASE_URL = "http://127.0.0.1:8000"
|
||||
|
||||
fun resetRealtimeProxyAsync() {
|
||||
thread(isDaemon = true) {
|
||||
try {
|
||||
val url = URL("$PROXY_BASE_URL/reset")
|
||||
val conn = url.openConnection() as HttpURLConnection
|
||||
conn.requestMethod = "POST"
|
||||
conn.setRequestProperty("Content-Type", "application/json")
|
||||
conn.doOutput = true
|
||||
conn.outputStream.use { it.write("{}".toByteArray()) }
|
||||
conn.inputStream.use { it.readBytes() } // просто чтобы завершить запрос корректно
|
||||
conn.disconnect()
|
||||
} catch (e: Exception) {
|
||||
// не ломаем диалог, если прокси недоступен
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Greeting: State = state(Parent) {
|
||||
onEntry {
|
||||
resetTrainingGameBag() // новый пользователь => новая случайная тройка игр
|
||||
TrainingMenuFlags.hasTrainedOnce = false;
|
||||
|
||||
resetRealtimeProxyAsync() // сброс прокси
|
||||
|
||||
println("user.dir = ${System.getProperty("user.dir")}")
|
||||
println("isLocalProjectRun = ${SessionLogger.isLocalProjectRun()}")
|
||||
println("isRunningOnRobot = ${SessionLogger.isRunningOnRobot()}")
|
||||
|
||||
SessionLogger.clearCsvIfRunningOnRobot()
|
||||
// SessionLogger.flushPendingLogs()
|
||||
SessionLogger.flushPendingLogsAsync()
|
||||
SessionLogger.startNewSession()
|
||||
|
||||
happyNod()
|
||||
furhat.say {
|
||||
random {
|
||||
+"Ahoj! Teším sa na náš rozhovor."
|
||||
+"Dobrý deň! Ďakujem, že ste prišli."
|
||||
+"Dobrý deň, vítam vás."
|
||||
+"Dobrý deň! Som ráda, že vás vidím."
|
||||
}
|
||||
}
|
||||
|
||||
furhat.say(pendingFileStatusMessage())
|
||||
|
||||
furhat.listen()
|
||||
}
|
||||
|
||||
onResponse<StopTraining> {
|
||||
if (handleStop(it.intent,
|
||||
"system",
|
||||
"greeting",
|
||||
it.text ?: ""))
|
||||
{
|
||||
TrainingMenuFlags.hasTrainedOnce = true
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
return@onResponse
|
||||
}
|
||||
}
|
||||
|
||||
onResponse<Repeat>{
|
||||
if (handleRepeat(
|
||||
it.intent,
|
||||
"repeat",
|
||||
"system",
|
||||
"greeting",
|
||||
it.text ?: ""
|
||||
))
|
||||
{
|
||||
return@onResponse
|
||||
}
|
||||
}
|
||||
|
||||
onResponse {
|
||||
if (handleRephrase(it.intent)){
|
||||
return@onResponse
|
||||
}
|
||||
|
||||
goto(CheckCondition)
|
||||
// goto(TimeTrainingIntro)
|
||||
// goto(MemoryTrainingIntro)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
package furhatos.app.blank.flow.main.handlers
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.flow.main.Idle
|
||||
import furhatos.app.blank.flow.main.supporting.calm
|
||||
import furhatos.flow.kotlin.*
|
||||
import furhatos.gestures.Gestures
|
||||
|
||||
val Goodbye: State = state(Parent) {
|
||||
|
||||
onEntry {
|
||||
furhat.gesture(Gestures.BigSmile, async = true)
|
||||
furhat.say {
|
||||
random {
|
||||
+"Ďakujem za dnešok. Prajem vám pekný deň!"
|
||||
+"Bolo mi potešením. Dovidenia!"
|
||||
+"Ďakujem, že ste so mnou cvičili. Majte sa krásne."
|
||||
+"Oddýchnite si a uvidíme sa nabudúce. Dovidenia!"
|
||||
+"Ak budete chcieť pokračovať, som tu pre vás. Dovidenia!"
|
||||
+"Majte sa dobre a dávajte na seba pozor. Dovidenia!"
|
||||
+"Dovidenia!"
|
||||
}
|
||||
}
|
||||
furhat.attendNobody()
|
||||
goto(Idle)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
package furhatos.app.blank.flow.main.handlers
|
||||
import furhatos.app.blank.flow.main.SessionLogger
|
||||
import furhatos.app.blank.flow.main.say_time.wrongAttempts
|
||||
import furhatos.app.blank.flow.main.supporting.happyNod
|
||||
import furhatos.app.blank.nlu.base_answer.Repeat
|
||||
import furhatos.flow.kotlin.*
|
||||
import furhatos.nlu.IntentInstance
|
||||
|
||||
var lastPhrase: String? = null
|
||||
var lastPhraseIsQuestion: Boolean = false
|
||||
|
||||
fun FlowControlRunner.askRepeatable(text: String) {
|
||||
lastPhrase = text
|
||||
lastPhraseIsQuestion = true
|
||||
furhat.ask(text)
|
||||
}
|
||||
|
||||
fun FlowControlRunner.sayRepeatable(text: String) {
|
||||
lastPhrase = text
|
||||
lastPhraseIsQuestion = false
|
||||
furhat.say(text)
|
||||
}
|
||||
|
||||
//fun FlowControlRunner.handleRepeat(intent: IntentInstance?): Boolean {
|
||||
// if (intent is Repeat) {
|
||||
// val phrase = lastPhrase
|
||||
// if (phrase != null) {
|
||||
// happyNod()
|
||||
// furhat.say("Samozrejme môžem zopakovať.")
|
||||
//
|
||||
// if (lastPhraseIsQuestion) {
|
||||
// furhat.ask(phrase)
|
||||
// } else {
|
||||
// furhat.say(phrase)
|
||||
// furhat.listen()
|
||||
// }
|
||||
// } else {
|
||||
// furhat.say("Prepáčte, momentálne nemám čo zopakovať.")
|
||||
// furhat.listen()
|
||||
// }
|
||||
// return true
|
||||
// }
|
||||
// return false
|
||||
//}
|
||||
|
||||
fun FlowControlRunner.handleRepeat(
|
||||
intent: IntentInstance?,
|
||||
game: String,
|
||||
question: String,
|
||||
correctAnswer: String = "",
|
||||
userAnswer: String = "",
|
||||
hintUsed: Boolean = false
|
||||
): Boolean {
|
||||
if (intent is Repeat) {
|
||||
val phrase = lastPhrase
|
||||
|
||||
SessionLogger.log(
|
||||
game = game,
|
||||
phase = "event",
|
||||
question = question,
|
||||
attempt = wrongAttempts + 1,
|
||||
result = "repeat_requested",
|
||||
correctAnswer = correctAnswer,
|
||||
userAnswer = userAnswer,
|
||||
hintUsed = hintUsed
|
||||
)
|
||||
|
||||
if (phrase != null) {
|
||||
happyNod()
|
||||
furhat.say("Samozrejme môžem zopakovať.")
|
||||
|
||||
if (lastPhraseIsQuestion) {
|
||||
furhat.ask(phrase)
|
||||
} else {
|
||||
furhat.say(phrase)
|
||||
furhat.listen()
|
||||
}
|
||||
} else {
|
||||
furhat.say("Prepáčte, momentálne nemám čo zopakovať.")
|
||||
furhat.listen()
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
package furhatos.app.blank.flow.main.handlers
|
||||
|
||||
import furhatos.app.blank.flow.main.supporting.general.callProxyRespond
|
||||
import furhatos.app.blank.flow.main.supporting.general.isProxyAvailable
|
||||
import furhatos.app.blank.flow.main.supporting.veryHappy
|
||||
import furhatos.app.blank.nlu.base_answer.Rephrase
|
||||
import furhatos.flow.kotlin.FlowControlRunner
|
||||
import furhatos.flow.kotlin.furhat
|
||||
import furhatos.nlu.IntentInstance
|
||||
|
||||
fun FlowControlRunner.handleRephrase(intent: IntentInstance?): Boolean {
|
||||
if (intent is Rephrase) {
|
||||
val phrase = lastPhrase
|
||||
|
||||
if (phrase != null) {
|
||||
if (isProxyAvailable()) {
|
||||
val rephrased = callProxyRespond(
|
||||
userText = phrase,
|
||||
task = "rephrase",
|
||||
context = mapOf(
|
||||
"original_question" to phrase
|
||||
)
|
||||
)
|
||||
|
||||
if (!rephrased.isNullOrBlank()) {
|
||||
veryHappy()
|
||||
furhat.say("Poviem to pre Vas inak.")
|
||||
askRepeatable(rephrased)
|
||||
}
|
||||
}else{
|
||||
furhat.say("Poviem to pre Vas inak.")
|
||||
askRepeatable(phrase)
|
||||
}
|
||||
} else {
|
||||
furhat.say("Prepáčte, momentálne nemám čo preformulovať.")
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package furhatos.app.blank.flow.main.handlers
|
||||
|
||||
import furhatos.app.blank.flow.main.SessionLogger
|
||||
import furhatos.app.blank.flow.main.say_time.hintUsedForCurrentQuestion
|
||||
import furhatos.app.blank.flow.main.say_time.questionsSinceLastCheck
|
||||
import furhatos.app.blank.flow.main.say_time.wrongAttempts
|
||||
import furhatos.app.blank.flow.main.supporting.empathy
|
||||
import furhatos.app.blank.nlu.base_answer.StopTraining
|
||||
import furhatos.flow.kotlin.FlowControlRunner
|
||||
import furhatos.flow.kotlin.furhat
|
||||
import furhatos.nlu.IntentInstance
|
||||
|
||||
//fun FlowControlRunner.handleStop(intent: IntentInstance?): Boolean {
|
||||
// if (intent is StopTraining) {
|
||||
// empathy()
|
||||
// furhat.say{
|
||||
// random{
|
||||
// +"Rozumiem. Zastavíme to, nič sa nedeje."
|
||||
// +"Dobre, zastavme sa tu."
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// wrongAttempts = 0
|
||||
// questionsSinceLastCheck = 0
|
||||
// return true
|
||||
// }
|
||||
// return false
|
||||
//}
|
||||
|
||||
fun FlowControlRunner.handleStop(
|
||||
intent: IntentInstance?,
|
||||
game: String,
|
||||
question: String,
|
||||
userAnswer: String = ""
|
||||
): Boolean {
|
||||
if (intent is StopTraining) {
|
||||
println("DEBUG handleStop: StopTraining matched")
|
||||
SessionLogger.log(
|
||||
game = game,
|
||||
phase = "event",
|
||||
question = question,
|
||||
attempt = wrongAttempts + 1,
|
||||
result = "stop_requested",
|
||||
correctAnswer = "",
|
||||
userAnswer = userAnswer,
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)
|
||||
|
||||
empathy()
|
||||
furhat.say {
|
||||
random {
|
||||
+"Rozumiem. Zastavíme to, nič sa nedeje."
|
||||
+"Dobre, zastavme sa tu."
|
||||
}
|
||||
}
|
||||
|
||||
wrongAttempts = 0
|
||||
questionsSinceLastCheck = 0
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
27
src/main/kotlin/furhatos/app/blank/flow/main/idle.kt
Normal file
27
src/main/kotlin/furhatos/app/blank/flow/main/idle.kt
Normal file
@ -0,0 +1,27 @@
|
||||
package furhatos.app.blank.flow.main
|
||||
|
||||
import furhatos.app.blank.flow.main.supporting.general.Test
|
||||
import furhatos.flow.kotlin.State
|
||||
import furhatos.flow.kotlin.furhat
|
||||
import furhatos.flow.kotlin.onUserEnter
|
||||
import furhatos.flow.kotlin.onUserLeave
|
||||
import furhatos.flow.kotlin.state
|
||||
|
||||
val Idle: State = state {
|
||||
onEntry {
|
||||
furhat.attendNobody()
|
||||
}
|
||||
|
||||
onUserEnter {
|
||||
furhat.attend(it)
|
||||
goto(Greeting)
|
||||
// goto(Test)
|
||||
}
|
||||
|
||||
onUserLeave(){
|
||||
// furhat.attend(it)
|
||||
// sayGoodbye()
|
||||
furhat.attendNobody()
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,642 @@
|
||||
package furhatos.app.blank.flow.main.memory
|
||||
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.flow.main.SessionLogger
|
||||
import furhatos.app.blank.flow.main.StartQuestion
|
||||
import furhatos.app.blank.flow.main.handlers.Goodbye
|
||||
import furhatos.app.blank.flow.main.handlers.handleRepeat
|
||||
import furhatos.app.blank.flow.main.handlers.handleStop
|
||||
import furhatos.app.blank.flow.main.supporting.AskToContinue
|
||||
import furhatos.app.blank.flow.main.supporting.general.WordBank
|
||||
import furhatos.app.blank.flow.main.supporting.general.WordEntry
|
||||
import furhatos.app.blank.flow.main.supporting.empathy
|
||||
import furhatos.app.blank.flow.main.supporting.sayPhraseForWrongAnswer
|
||||
import furhatos.app.blank.flow.main.supporting.littleSad
|
||||
import furhatos.app.blank.flow.main.supporting.veryHappy
|
||||
import furhatos.app.blank.nlu.base_answer.DontKnow
|
||||
import furhatos.app.blank.nlu.base_answer.Help
|
||||
import furhatos.app.blank.nlu.base_answer.Repeat
|
||||
import furhatos.app.blank.nlu.base_answer.StopTraining
|
||||
import furhatos.flow.kotlin.*
|
||||
import kotlin.math.ceil
|
||||
import kotlin.random.Random
|
||||
import furhatos.app.blank.flow.main.say_time.wrongAttempts
|
||||
import furhatos.app.blank.flow.main.say_time.hintUsedForCurrentQuestion
|
||||
import furhatos.app.blank.flow.main.supporting.AskDecreaseDifficulty
|
||||
import furhatos.app.blank.flow.main.supporting.AskIncreaseDifficulty
|
||||
import furhatos.app.blank.flow.main.supporting.ReadyToTrain
|
||||
import furhatos.app.blank.flow.main.supporting.general.SmallTalkContext
|
||||
import furhatos.app.blank.flow.main.supporting.general.TrainingMenuFlags
|
||||
import furhatos.app.blank.flow.main.supporting.general.callProxyRespond
|
||||
import furhatos.app.blank.flow.main.supporting.general.genericSmallTalk
|
||||
import furhatos.app.blank.flow.main.supporting.general.isProxyAvailable
|
||||
import furhatos.app.blank.flow.main.supporting.general.requestSmallTalk
|
||||
import furhatos.app.blank.flow.main.supporting.general.sendLogEvent
|
||||
import furhatos.gestures.Gestures
|
||||
|
||||
// -------------------- parametre --------------------
|
||||
|
||||
private const val MAX_LEVEL = 3
|
||||
private const val QUESTIONS_PER_LEVEL = 2
|
||||
|
||||
private const val SUCCESS_THRESHOLD = 0.90
|
||||
private const val NEAR_SUCCESS_THRESHOLD = 0.60
|
||||
|
||||
private const val MAX_SAME_LEVEL_QUESTIONS = 6
|
||||
|
||||
var memoryLevel: Int = 1
|
||||
var sequencesDoneAtLevel: Int = 0
|
||||
|
||||
var sameLevelQuestionsDone: Int = 0
|
||||
var difficultyWasIncreased: Boolean = false
|
||||
|
||||
// koľko otázok v aktuálnom bloku z 2 bolo neúspešných
|
||||
var failedQuestions: Int = 0
|
||||
|
||||
// Bol aktuálny blok z 2 kôl začatý po zvýšení náročnosti
|
||||
var currentBlockWasIncreased: Boolean = false
|
||||
|
||||
private var sequencePresented: Boolean = false
|
||||
var currentSequence: List<WordEntry> = emptyList()
|
||||
|
||||
// slová používateľa po jednom
|
||||
val collectedTokens: MutableList<String> = mutableListOf()
|
||||
|
||||
// pozície chýb v poslednom pokuse
|
||||
var lastWrongPositions: List<Int> = emptyList()
|
||||
|
||||
var memoryHintReason: String = "general"
|
||||
|
||||
// -------------------- hodnotenie pokusu --------------------
|
||||
private fun requiredCorrectCount(n: Int, threshold: Double): Int =
|
||||
ceil(n * threshold).toInt().coerceAtLeast(1)
|
||||
|
||||
|
||||
private fun evaluateCollected(collected: List<String>, target: List<WordEntry>): Pair<Int, List<Int>> {
|
||||
val n = target.size
|
||||
var correct = 0
|
||||
val wrongPos = mutableListOf<Int>()
|
||||
|
||||
for (i in 0 until n) {
|
||||
val userTok = collected.getOrNull(i)
|
||||
val ok = userTok != null && WordsChecker.matchesWord(userTok, target[i])
|
||||
if (ok) correct++ else wrongPos.add(i + 1)
|
||||
}
|
||||
return Pair(correct, wrongPos)
|
||||
}
|
||||
|
||||
// -------------------- generovanie postupnosti --------------------
|
||||
|
||||
private fun lengthForLevel(level: Int, rng: Random = Random.Default): Int = when (level) {
|
||||
1 -> listOf(3, 3).random(rng)
|
||||
2 -> listOf(5, 6).random(rng)
|
||||
else -> listOf(6, 7).random(rng)
|
||||
}
|
||||
|
||||
private fun buildNewSequence() {
|
||||
val len = lengthForLevel(memoryLevel)
|
||||
currentSequence = WordBank.pickSequence(
|
||||
length = len,
|
||||
maxDifficulty = memoryLevel
|
||||
)
|
||||
sequencePresented = false
|
||||
lastWrongPositions = emptyList()
|
||||
collectedTokens.clear()
|
||||
}
|
||||
//==============================================================
|
||||
// Small Talk
|
||||
//==============================================================
|
||||
fun buildMemorySmallTalkContext(): SmallTalkContext {
|
||||
val theme = currentSequence.firstOrNull()?.theme ?: "memory"
|
||||
|
||||
return SmallTalkContext(
|
||||
exercise = "memory",
|
||||
topic = theme,
|
||||
subtopic = "sequence_recall",
|
||||
targetWord = currentSequence.firstOrNull()?.canonical,
|
||||
responseMode = "open"
|
||||
)
|
||||
}
|
||||
|
||||
fun memorySmallTalkSmart(nextState: State): State {
|
||||
val ctx = buildMemorySmallTalkContext()
|
||||
val proxyQuestion = requestSmallTalk(ctx)
|
||||
|
||||
return if (proxyQuestion.isNotBlank()) {
|
||||
genericSmallTalk(
|
||||
context = ctx,
|
||||
nextState = nextState,
|
||||
fallbackQuestion = "Spája sa Vám niektoré z týchto slov s niečím známym?",
|
||||
preparedQuestion = proxyQuestion
|
||||
)
|
||||
} else {
|
||||
genericSmallTalk(
|
||||
context = ctx,
|
||||
nextState = nextState,
|
||||
fallbackQuestion = "Spája sa Vám niektoré z týchto slov s niečím známym?"
|
||||
)
|
||||
}
|
||||
}
|
||||
//==============================================================
|
||||
// Hint
|
||||
//==============================================================
|
||||
|
||||
fun requestMemoryExplainWrong(mistakeType: String): String {
|
||||
val sequenceWords = currentSequence.map { it.canonical }.joinToString(", ")
|
||||
val pos = lastWrongPositions.distinct().sorted().joinToString(", ")
|
||||
|
||||
if (!isProxyAvailable()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return callProxyRespond(
|
||||
userText = "Používateľ urobil chybu v cvičení pamäte.",
|
||||
task = "explain_wrong",
|
||||
context = mapOf(
|
||||
"exercise" to "memory",
|
||||
"mistake_type" to mistakeType, // final_failure / partial_wrong
|
||||
"level" to memoryLevel,
|
||||
"sequence_length" to currentSequence.size,
|
||||
"target_sequence" to sequenceWords,
|
||||
"wrong_positions" to pos
|
||||
)
|
||||
) ?: ""
|
||||
}
|
||||
//===============================================
|
||||
// Proces hry
|
||||
//===============================================
|
||||
// Intro
|
||||
|
||||
val MemoryTrainingIntro: State = state(Parent) {
|
||||
onEntry {
|
||||
wrongAttempts = 0
|
||||
hintUsedForCurrentQuestion = false
|
||||
|
||||
sameLevelQuestionsDone = 0
|
||||
difficultyWasIncreased = false
|
||||
|
||||
failedQuestions = 0
|
||||
currentBlockWasIncreased = false
|
||||
|
||||
memoryLevel = 1
|
||||
sequencesDoneAtLevel = 0
|
||||
|
||||
buildNewSequence()
|
||||
goto(AskSequence)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- AskSequence - zobrazuje zoznam 1 raz, potom prejde k zberu --------------------
|
||||
|
||||
val AskSequence: State = state(Parent) {
|
||||
|
||||
onEntry {
|
||||
furhat.say("Teraz pomenujem niekoľko pojmov.")
|
||||
|
||||
if (!sequencePresented) {
|
||||
furhat.say("Pozorne počúvajte a zapamätajte si ich.")
|
||||
//delay(1000)
|
||||
furhat.say("Poradie slov je také:")
|
||||
delay(1000)
|
||||
|
||||
currentSequence.forEachIndexed { i, w ->
|
||||
furhat.say(w.canonical)
|
||||
if (i != currentSequence.lastIndex) delay(1700)
|
||||
}
|
||||
|
||||
delay(1300)
|
||||
furhat.say("Skúste ich teraz zopakovať v rovnakom poradí.")
|
||||
sequencePresented = true
|
||||
|
||||
memoryLogWithDebug("question_shown / asked"){
|
||||
SessionLogger.log(
|
||||
game = "memory",
|
||||
phase = "question_shown",
|
||||
question = "LEVEL_${memoryLevel}_LEN_${currentSequence.size}",
|
||||
attempt = wrongAttempts + 1,
|
||||
result = "asked",
|
||||
correctAnswer = currentSequence.joinToString(", ") { it.canonical },
|
||||
userAnswer = "",
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)}
|
||||
}
|
||||
|
||||
collectedTokens.clear()
|
||||
goto(CollectSequence)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- CollectSequence: pocuvanie slov ----------------
|
||||
|
||||
val CollectSequence: State = state(Parent) {
|
||||
|
||||
onEntry {
|
||||
furhat.param.endSilTimeout = 400
|
||||
furhat.attend(users.current)
|
||||
|
||||
furhat.listen()
|
||||
}
|
||||
|
||||
onResponse<DontKnow> {
|
||||
memoryLogWithDebug("Don't know"){
|
||||
SessionLogger.log(
|
||||
game = "memory",
|
||||
phase = "event",
|
||||
question = "LEVEL_${memoryLevel}_LEN_${currentSequence.size}",
|
||||
attempt = wrongAttempts + 1,
|
||||
result = "dont_know",
|
||||
correctAnswer = currentSequence.joinToString(", ") { it.canonical },
|
||||
userAnswer = it.text ?: "",
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)}
|
||||
|
||||
empathy()
|
||||
furhat.say("Rozumiem, to je v poriadku.")
|
||||
|
||||
if (!hintUsedForCurrentQuestion) {
|
||||
memoryHintReason = "dont_know"
|
||||
if (wrongAttempts < 2) wrongAttempts = 2
|
||||
goto(MemoryHintOffer)
|
||||
return@onResponse
|
||||
}
|
||||
|
||||
// napoveda uz bola
|
||||
wrongAttempts = 3
|
||||
lastWrongPositions = (1..currentSequence.size).toList()
|
||||
goto(AfterSequenceResolved)
|
||||
}
|
||||
|
||||
onResponse<Help> {
|
||||
memoryLogWithDebug("Help"){
|
||||
SessionLogger.log(
|
||||
game = "memory",
|
||||
phase = "event",
|
||||
question = "LEVEL_${memoryLevel}_LEN_${currentSequence.size}",
|
||||
attempt = wrongAttempts + 1,
|
||||
result = "help_requested",
|
||||
correctAnswer = currentSequence.joinToString(", ") { it.canonical },
|
||||
userAnswer = it.text ?: "",
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)}
|
||||
|
||||
if (!hintUsedForCurrentQuestion) {
|
||||
veryHappy()
|
||||
|
||||
memoryHintReason = "help"
|
||||
furhat.say("Samozrejme, pomôžem Vám!")
|
||||
|
||||
if (wrongAttempts < 2) wrongAttempts = 2
|
||||
goto(MemoryHintOffer)
|
||||
return@onResponse
|
||||
}
|
||||
|
||||
empathy()
|
||||
furhat.say("Už som Vám raz zopakoval zoznam.")
|
||||
|
||||
furhat.gesture(Gestures.ExpressSad(strength = 0.35, duration = 0.8), async = true)
|
||||
furhat.say("Je mi ľúto, že Vám nepomohol. Skúste to ešte raz.")
|
||||
collectedTokens.clear()
|
||||
reentry()
|
||||
}
|
||||
|
||||
onResponse<StopTraining> {
|
||||
if (handleStop(it.intent,
|
||||
"memory",
|
||||
"LEVEL_${memoryLevel}_LEN_${currentSequence.size}",
|
||||
it.text ?: ""))
|
||||
{
|
||||
TrainingMenuFlags.hasTrainedOnce = true
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
return@onResponse
|
||||
}
|
||||
}
|
||||
|
||||
// onResponse<Repeat> {
|
||||
// if (handleRepeat(
|
||||
// it.intent,
|
||||
// "memory",
|
||||
// "LEVEL_${memoryLevel}_LEN_${currentSequence.size}",
|
||||
// currentSequence.joinToString(", ") { it.canonical },
|
||||
// it.text ?: "",
|
||||
// true
|
||||
// ))
|
||||
// {
|
||||
// return@onResponse
|
||||
// }
|
||||
// }
|
||||
|
||||
onResponse {
|
||||
if (it.intent is Repeat) {
|
||||
SessionLogger.log(
|
||||
game = "memory",
|
||||
phase = "event",
|
||||
question = "LEVEL_${memoryLevel}_LEN_${currentSequence.size}",
|
||||
attempt = wrongAttempts + 1,
|
||||
result = "repeat_requested",
|
||||
correctAnswer = currentSequence.joinToString(", ") { it.canonical },
|
||||
userAnswer = it.text ?: "",
|
||||
hintUsed = true
|
||||
)
|
||||
|
||||
hintUsedForCurrentQuestion = true
|
||||
collectedTokens.clear()
|
||||
|
||||
furhat.say("Zopakujem Vám zoznam ešte raz.")
|
||||
delay(1000)
|
||||
furhat.say("Slová sú:")
|
||||
delay(1000)
|
||||
currentSequence.forEachIndexed { i, w ->
|
||||
furhat.say(w.canonical)
|
||||
if (i != currentSequence.lastIndex) delay(1200)
|
||||
}
|
||||
reentry()
|
||||
return@onResponse
|
||||
}
|
||||
|
||||
val text = it.text ?: ""
|
||||
val token = WordsChecker.tokenizeMeaningful(text).firstOrNull()
|
||||
|
||||
if (token == null) {
|
||||
// nic zmyslene nebolo
|
||||
|
||||
memoryLogWithDebug("Zber sekvencii - null"){
|
||||
SessionLogger.log(
|
||||
game = "memory",
|
||||
phase = "event",
|
||||
question = "LEVEL_${memoryLevel}_LEN_${currentSequence.size}",
|
||||
attempt = wrongAttempts + 1,
|
||||
result = "unrecognized_token",
|
||||
correctAnswer = currentSequence.joinToString(", ") { it.canonical },
|
||||
userAnswer = text,
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)}
|
||||
|
||||
littleSad()
|
||||
reentry()
|
||||
return@onResponse
|
||||
}
|
||||
|
||||
collectedTokens.add(token)
|
||||
|
||||
furhat.gesture(Gestures.Nod, async = true)
|
||||
|
||||
if (collectedTokens.size < currentSequence.size) {
|
||||
reentry()
|
||||
return@onResponse
|
||||
}
|
||||
|
||||
// pokus je cely -> hodnotenie
|
||||
val n = currentSequence.size
|
||||
val requiredSuccess = requiredCorrectCount(n, SUCCESS_THRESHOLD)
|
||||
val requiredNear = requiredCorrectCount(n, NEAR_SUCCESS_THRESHOLD)
|
||||
|
||||
val (correct, wrongPos) = evaluateCollected(collectedTokens, currentSequence)
|
||||
lastWrongPositions = wrongPos
|
||||
|
||||
val targetSequence = currentSequence.joinToString(", ") { it.canonical }
|
||||
val userSequence = collectedTokens.joinToString(", ")
|
||||
|
||||
val resultForLog = when {
|
||||
correct >= requiredSuccess -> "success"
|
||||
correct >= requiredNear -> "partial_success"
|
||||
else -> "fail"
|
||||
}
|
||||
|
||||
memoryLogWithDebug("Zber sekvencii / answer"){
|
||||
SessionLogger.log(
|
||||
game = "memory",
|
||||
phase = "answer",
|
||||
question = "LEVEL_${memoryLevel}_LEN_${currentSequence.size}",
|
||||
attempt = wrongAttempts + 1,
|
||||
result = resultForLog,
|
||||
correctAnswer = targetSequence,
|
||||
userAnswer = userSequence,
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)}
|
||||
|
||||
when {
|
||||
// full uspech
|
||||
correct >= requiredSuccess -> {
|
||||
wrongAttempts = 0
|
||||
hintUsedForCurrentQuestion = false
|
||||
|
||||
veryHappy()
|
||||
furhat.say("Výborne! To bolo správne.")
|
||||
delay(1000)
|
||||
|
||||
goto(memorySmallTalkSmart(AfterSequenceResolved))
|
||||
return@onResponse
|
||||
}
|
||||
|
||||
// >= 60% a < 80%
|
||||
correct >= requiredNear -> {
|
||||
wrongAttempts = 0
|
||||
hintUsedForCurrentQuestion = false
|
||||
|
||||
veryHappy()
|
||||
furhat.say("Takmer všetky slová ste pomenovali správne!")
|
||||
delay(1000)
|
||||
|
||||
goto(memorySmallTalkSmart(AfterSequenceResolved))
|
||||
return@onResponse
|
||||
}
|
||||
|
||||
else -> {
|
||||
wrongAttempts++
|
||||
when (wrongAttempts) {
|
||||
1 -> {
|
||||
val hint = requestMemoryHint("partial_wrong")
|
||||
|
||||
if (hint.isNotBlank()){
|
||||
furhat.say("Dajte mi chvíľu. Pokúsim sa Vám pomôcť.")
|
||||
furhat.say(hint)
|
||||
}
|
||||
else{
|
||||
littleSad()
|
||||
furhat.say("Nie celkom správne. Skúste to ešte raz.")
|
||||
}
|
||||
|
||||
|
||||
collectedTokens.clear()
|
||||
reentry()
|
||||
}
|
||||
2 -> {
|
||||
sayPhraseForWrongAnswer()
|
||||
collectedTokens.clear()
|
||||
goto(MemoryHintOffer)
|
||||
}
|
||||
else -> goto(AfterSequenceResolved)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onNoResponse {
|
||||
memoryLogWithDebug("No response"){
|
||||
SessionLogger.log(
|
||||
game = "memory",
|
||||
phase = "answer",
|
||||
question = "LEVEL_${memoryLevel}_LEN_${currentSequence.size}",
|
||||
attempt = wrongAttempts + 1,
|
||||
result = "no_response",
|
||||
correctAnswer = currentSequence.joinToString(", ") { it.canonical },
|
||||
userAnswer = "",
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)}
|
||||
|
||||
if (!hintUsedForCurrentQuestion) {
|
||||
if (wrongAttempts < 2) wrongAttempts = 2
|
||||
goto(MemoryHintOffer)
|
||||
} else {
|
||||
wrongAttempts = 3
|
||||
lastWrongPositions = (1..currentSequence.size).toList()
|
||||
goto(AfterSequenceResolved)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- prechod medzi urovni --------------------
|
||||
|
||||
val AfterSequenceResolved: State = state(Parent) {
|
||||
onEntry {
|
||||
if (wrongAttempts >= 3) {
|
||||
memoryLogWithDebug("Resolved sequence"){
|
||||
SessionLogger.log(
|
||||
game = "memory",
|
||||
phase = "resolved",
|
||||
question = "LEVEL_${memoryLevel}_LEN_${currentSequence.size}",
|
||||
attempt = wrongAttempts,
|
||||
result = "final_fail",
|
||||
correctAnswer = currentSequence.joinToString(", ") { it.canonical },
|
||||
userAnswer = collectedTokens.joinToString(", "),
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)}
|
||||
|
||||
val pos = lastWrongPositions.distinct().sorted().joinToString(", ")
|
||||
val explanation = requestMemoryExplainWrong("final_failure")
|
||||
|
||||
littleSad()
|
||||
furhat.say("Je mi ľúto, ale nedali ste správnu odpoveď. Tentoraz to nevyšlo, ale to nevadí.")
|
||||
|
||||
if (explanation.isNotBlank()){
|
||||
furhat.say(explanation)
|
||||
}
|
||||
else{
|
||||
furhat.say("V poslednej odpovedi boli nesprávne slová na pozíciách: $pos.")
|
||||
}
|
||||
|
||||
delay(1000)
|
||||
}
|
||||
|
||||
// zatvorenie otazky
|
||||
val questionFailed = wrongAttempts >= 3
|
||||
wrongAttempts = 0
|
||||
hintUsedForCurrentQuestion = false
|
||||
collectedTokens.clear()
|
||||
lastWrongPositions = emptyList()
|
||||
|
||||
sequencesDoneAtLevel++
|
||||
|
||||
if (questionFailed) {
|
||||
failedQuestions++
|
||||
}
|
||||
|
||||
if (!difficultyWasIncreased) {
|
||||
sameLevelQuestionsDone++
|
||||
|
||||
if (sameLevelQuestionsDone >= MAX_SAME_LEVEL_QUESTIONS) {
|
||||
veryHappy()
|
||||
furhat.say("Výborne, dnes už stačí. Ukončíme tuto hru na pamäť. Ďakujem Vám!")
|
||||
delay(1000)
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
return@onEntry
|
||||
}
|
||||
}
|
||||
|
||||
if (sequencesDoneAtLevel < QUESTIONS_PER_LEVEL) {
|
||||
buildNewSequence()
|
||||
goto(AskSequence)
|
||||
return@onEntry
|
||||
}
|
||||
|
||||
if (memoryLevel >= MAX_LEVEL) {
|
||||
veryHappy()
|
||||
furhat.say("Týmto sme ukončili dnešnú hru na pamäť. Ďakujem Vám!")
|
||||
delay(1000)
|
||||
|
||||
TrainingMenuFlags.allMemoryQuestionsCompleted = true
|
||||
TrainingMenuFlags.hasTrainedOnce = true
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
return@onEntry
|
||||
}
|
||||
|
||||
val wantsToContinue = call(AskToContinue()) as Boolean
|
||||
if (!wantsToContinue) {
|
||||
TrainingMenuFlags.hasTrainedOnce = true
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
return@onEntry
|
||||
}
|
||||
|
||||
// vrátiť sa, ak boli obe otázky na novej úrovni neúspešné
|
||||
if (currentBlockWasIncreased && failedQuestions >= QUESTIONS_PER_LEVEL && memoryLevel > 1) {
|
||||
|
||||
val goBack = call(AskDecreaseDifficulty) as Boolean
|
||||
|
||||
if (goBack) {
|
||||
memoryLevel--
|
||||
furhat.say("Dobre, vrátime sa na ľahšiu úroveň.")
|
||||
} else {
|
||||
furhat.say("Dobre, zostaneme na rovnakej úrovni.")
|
||||
}
|
||||
|
||||
sequencesDoneAtLevel = 0
|
||||
failedQuestions = 0
|
||||
currentBlockWasIncreased = false
|
||||
|
||||
buildNewSequence()
|
||||
goto(AskSequence)
|
||||
return@onEntry
|
||||
}
|
||||
|
||||
|
||||
val increase = call(AskIncreaseDifficulty) as Boolean
|
||||
if (increase && memoryLevel < MAX_LEVEL) {
|
||||
difficultyWasIncreased = true
|
||||
memoryLevel++
|
||||
currentBlockWasIncreased = true
|
||||
furhat.say("Dobre, zvýšime náročnosť.")
|
||||
} else {
|
||||
currentBlockWasIncreased = false
|
||||
furhat.say("Dobre, zostaneme na rovnakej úrovni.")
|
||||
}
|
||||
|
||||
sequencesDoneAtLevel = 0
|
||||
failedQuestions = 0
|
||||
buildNewSequence()
|
||||
goto(AskSequence)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun memoryLogDebug(stage: String) {
|
||||
val now = java.time.LocalTime.now()
|
||||
println("[MEMORY DEBUG $now] $stage")
|
||||
}
|
||||
|
||||
private inline fun memoryLogWithDebug(
|
||||
stage: String,
|
||||
block: () -> Unit
|
||||
) {
|
||||
val start = System.currentTimeMillis()
|
||||
memoryLogDebug("START log -> $stage")
|
||||
|
||||
try {
|
||||
block()
|
||||
val elapsed = System.currentTimeMillis() - start
|
||||
memoryLogDebug("END log -> $stage (${elapsed} ms)")
|
||||
} catch (e: Exception) {
|
||||
val elapsed = System.currentTimeMillis() - start
|
||||
memoryLogDebug("ERROR log -> $stage (${elapsed} ms): ${e.message}")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package furhatos.app.blank.flow.main.memory
|
||||
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.flow.main.say_time.hintUsedForCurrentQuestion
|
||||
import furhatos.app.blank.flow.main.supporting.HintOffer
|
||||
import furhatos.app.blank.flow.main.supporting.general.callProxyRespond
|
||||
import furhatos.app.blank.flow.main.supporting.general.isProxyAvailable
|
||||
import furhatos.app.blank.flow.main.supporting.veryHappy
|
||||
import furhatos.flow.kotlin.State
|
||||
import furhatos.flow.kotlin.furhat
|
||||
import furhatos.flow.kotlin.state
|
||||
|
||||
fun requestMemoryHint(mistakeType: String): String {
|
||||
val sequenceWords = currentSequence.map { it.canonical }.joinToString(", ")
|
||||
|
||||
if (!isProxyAvailable()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return callProxyRespond(
|
||||
userText = "Používateľ potrebuje nápovedu v cvičení pamäte.",
|
||||
task = "hint",
|
||||
context = mapOf(
|
||||
"exercise" to "memory",
|
||||
"mistake_type" to mistakeType, // partial_wrong / dont_know / no_response / help
|
||||
"level" to memoryLevel,
|
||||
"sequence_length" to currentSequence.size,
|
||||
"target_sequence" to sequenceWords,
|
||||
"wrong_positions" to lastWrongPositions.joinToString(", ")
|
||||
)
|
||||
) ?: ""
|
||||
}
|
||||
|
||||
val MemoryHintOffer: State by lazy {
|
||||
HintOffer(nextState = MemoryHint, exitState = CollectSequence)
|
||||
}
|
||||
|
||||
val MemoryHint: State = state(Parent) {
|
||||
onEntry {
|
||||
hintUsedForCurrentQuestion = true
|
||||
collectedTokens.clear()
|
||||
|
||||
val proxyHint = requestMemoryHint(memoryHintReason)
|
||||
|
||||
if (proxyHint.isNotBlank()){
|
||||
furhat.say(proxyHint)
|
||||
}
|
||||
else{
|
||||
veryHappy()
|
||||
furhat.say("Dobre! Zopakujem Vám zoznam ešte raz.")
|
||||
|
||||
furhat.say("Slová sú:")
|
||||
|
||||
currentSequence.forEachIndexed { i, w ->
|
||||
furhat.say(w.canonical)
|
||||
if (i != currentSequence.lastIndex) delay(1300)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
furhat.say("Prosím, pokračujte.")
|
||||
goto(CollectSequence)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package furhatos.app.blank.flow.main.memory
|
||||
|
||||
import furhatos.app.blank.flow.main.supporting.general.WordEntry
|
||||
import java.text.Normalizer
|
||||
|
||||
// -------------------- zbytocne slova --------------------
|
||||
private val STOPWORDS: Set<String> = setOf(
|
||||
"eee", "ehm", "hm", "hmm", "mmm",
|
||||
"no", "tak", "teda", "proste", "akoze", "akože", "vlastne",
|
||||
"prosím", "prosim", "prosímťa", "prosimta",
|
||||
"a", "aj", "že", "ze", "potom", "takže", "takze", "iii", "i"
|
||||
)
|
||||
|
||||
object WordsChecker {
|
||||
// normalizacia
|
||||
private fun stripDiacritics(s: String): String {
|
||||
val norm = Normalizer.normalize(s, Normalizer.Form.NFD)
|
||||
return norm.replace("\\p{Mn}+".toRegex(), "")
|
||||
}
|
||||
|
||||
private fun normalizeToken(token: String): String =
|
||||
stripDiacritics(token.lowercase())
|
||||
.replace("[^a-z0-9]".toRegex(), "")
|
||||
|
||||
fun tokenizeMeaningful(text: String): List<String> {
|
||||
val cleaned = text
|
||||
.lowercase()
|
||||
.replace("[,.;:!?()\\[\\]{}\"“”„–—]".toRegex(), " ")
|
||||
.replace("\\s+".toRegex(), " ")
|
||||
.trim()
|
||||
|
||||
if (cleaned.isEmpty()) return emptyList()
|
||||
|
||||
return cleaned.split(" ")
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.filter { normalizeToken(it).isNotEmpty() }
|
||||
.filter { normalizeToken(it) !in STOPWORDS }
|
||||
}
|
||||
|
||||
fun matchesWord(userToken: String, target: WordEntry): Boolean {
|
||||
val userNorm = normalizeToken(userToken)
|
||||
if (userNorm.isEmpty()) return false
|
||||
|
||||
val acceptable = (target.variants + target.canonical)
|
||||
.map { normalizeToken(it) }
|
||||
.filter { it.isNotEmpty() }
|
||||
.toSet()
|
||||
|
||||
return userNorm in acceptable
|
||||
}
|
||||
}
|
||||
907
src/main/kotlin/furhatos/app/blank/flow/main/say_time/AskTime.kt
Normal file
907
src/main/kotlin/furhatos/app/blank/flow/main/say_time/AskTime.kt
Normal file
@ -0,0 +1,907 @@
|
||||
package furhatos.app.blank.flow.main.say_time
|
||||
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.flow.main.SessionLogger
|
||||
import furhatos.app.blank.flow.main.StartQuestion
|
||||
import furhatos.app.blank.flow.main.handlers.Goodbye
|
||||
import furhatos.app.blank.flow.main.handlers.askRepeatable
|
||||
import furhatos.app.blank.flow.main.handlers.handleRepeat
|
||||
import furhatos.app.blank.flow.main.handlers.handleRephrase
|
||||
import furhatos.app.blank.flow.main.handlers.handleStop
|
||||
import furhatos.app.blank.nlu.base_answer.StopTraining
|
||||
import furhatos.app.blank.flow.main.memory.currentSequence
|
||||
import furhatos.app.blank.flow.main.memory.memoryLevel
|
||||
import furhatos.app.blank.flow.main.pendingFileStatusMessage
|
||||
import furhatos.app.blank.flow.main.supporting.AskToContinue
|
||||
import furhatos.app.blank.nlu.base_answer.DontKnow
|
||||
import furhatos.app.blank.nlu.base_answer.Rephrase
|
||||
import furhatos.app.blank.nlu.base_answer.Help
|
||||
import furhatos.app.blank.flow.main.supporting.CurrentMonthSmallTalk
|
||||
import furhatos.app.blank.flow.main.supporting.CurrentTimeSmallTalk
|
||||
import furhatos.app.blank.flow.main.supporting.DayPeriodSmallTalk
|
||||
import furhatos.app.blank.flow.main.supporting.ReadyToTrain
|
||||
import furhatos.app.blank.flow.main.supporting.TodayDateSmallTalk
|
||||
import furhatos.app.blank.flow.main.supporting.TomorrowDateSmallTalk
|
||||
import furhatos.app.blank.flow.main.supporting.general.TrainingMenuFlags
|
||||
import furhatos.app.blank.flow.main.supporting.WeekdaySmallTalk
|
||||
import furhatos.app.blank.flow.main.supporting.calm
|
||||
import furhatos.app.blank.flow.main.supporting.empathy
|
||||
import furhatos.app.blank.flow.main.supporting.happyShake
|
||||
import furhatos.app.blank.flow.main.supporting.littleSad
|
||||
import furhatos.app.blank.flow.main.supporting.explainWhyWrong
|
||||
import furhatos.app.blank.flow.main.supporting.general.SmallTalkContext
|
||||
import furhatos.app.blank.flow.main.supporting.general.genericSmallTalk
|
||||
import furhatos.app.blank.flow.main.supporting.general.requestSmallTalk
|
||||
import furhatos.app.blank.flow.main.supporting.sayPhraseForWrongAnswer
|
||||
import furhatos.app.blank.flow.main.supporting.veryHappy
|
||||
import furhatos.app.blank.nlu.base_answer.Repeat
|
||||
import furhatos.flow.kotlin.State
|
||||
import furhatos.flow.kotlin.furhat
|
||||
import furhatos.flow.kotlin.state
|
||||
import furhatos.flow.kotlin.*
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
var TimeQuestionTypes: MutableList<TimeQuestionType> = mutableListOf(
|
||||
TimeQuestionType.CURRENT_TIME,
|
||||
TimeQuestionType.TODAY_DATE,
|
||||
TimeQuestionType.TOMORROW_DATE,
|
||||
TimeQuestionType.CURRENT_MONTH,
|
||||
TimeQuestionType.TODAY_WEEKDAY,
|
||||
TimeQuestionType.DAY_PERIOD
|
||||
)
|
||||
|
||||
val priorityTimeQuestionOrder: List<TimeQuestionType> = listOf(
|
||||
TimeQuestionType.CURRENT_MONTH,
|
||||
TimeQuestionType.TODAY_DATE,
|
||||
TimeQuestionType.TOMORROW_DATE
|
||||
)
|
||||
|
||||
var priorityIndex: Int = 0
|
||||
private const val MAX_SMALLTALK_PER_SESSION = 3 // половина из 6
|
||||
var smallTalkUsed = 0
|
||||
|
||||
|
||||
//==============================================================
|
||||
// Small Talk
|
||||
//==============================================================
|
||||
fun buildSmallTalkContextTime(question: TimeQuestionType): SmallTalkContext {
|
||||
return when (question) {
|
||||
TimeQuestionType.CURRENT_TIME -> SmallTalkContext(
|
||||
exercise = "time",
|
||||
topic = "time",
|
||||
subtopic = "current_time",
|
||||
responseMode = "open"
|
||||
)
|
||||
|
||||
TimeQuestionType.TODAY_DATE -> SmallTalkContext(
|
||||
exercise = "time",
|
||||
topic = "date",
|
||||
subtopic = "today_date",
|
||||
responseMode = "open"
|
||||
)
|
||||
|
||||
TimeQuestionType.TOMORROW_DATE -> SmallTalkContext(
|
||||
exercise = "time",
|
||||
topic = "date",
|
||||
subtopic = "tomorrow_date",
|
||||
responseMode = "open"
|
||||
)
|
||||
|
||||
TimeQuestionType.CURRENT_MONTH -> SmallTalkContext(
|
||||
exercise = "time",
|
||||
topic = "month",
|
||||
subtopic = "current_month",
|
||||
responseMode = "open"
|
||||
)
|
||||
|
||||
TimeQuestionType.TODAY_WEEKDAY -> SmallTalkContext(
|
||||
exercise = "time",
|
||||
topic = "weekday",
|
||||
subtopic = DateTimeChecker.CorrectTodayWeekday(),
|
||||
responseMode = "open"
|
||||
)
|
||||
|
||||
TimeQuestionType.DAY_PERIOD -> SmallTalkContext(
|
||||
exercise = "time",
|
||||
topic = "day_period",
|
||||
subtopic = lastDayPeriod,
|
||||
responseMode = "yes_no"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun fallbackSmallTalk(question: TimeQuestionType): State {
|
||||
return when (question) {
|
||||
TimeQuestionType.CURRENT_TIME -> CurrentTimeSmallTalk
|
||||
TimeQuestionType.TODAY_DATE -> TodayDateSmallTalk
|
||||
TimeQuestionType.TOMORROW_DATE -> TomorrowDateSmallTalk
|
||||
TimeQuestionType.CURRENT_MONTH -> CurrentMonthSmallTalk
|
||||
TimeQuestionType.TODAY_WEEKDAY -> WeekdaySmallTalk
|
||||
TimeQuestionType.DAY_PERIOD -> DayPeriodSmallTalk
|
||||
}
|
||||
}
|
||||
|
||||
fun smallTalkManagerTime(question: TimeQuestionType): State {
|
||||
if (smallTalkUsed >= MAX_SMALLTALK_PER_SESSION) return TimeTrainingSuccess
|
||||
|
||||
// kolko otazok este su v liste
|
||||
val denom = TimeQuestionTypes.size + 1
|
||||
val smallTalkLeft = MAX_SMALLTALK_PER_SESSION - smallTalkUsed
|
||||
|
||||
// výpočty šancov
|
||||
val percent = minOf(0.5, smallTalkLeft.toDouble() / denom.toDouble())
|
||||
val doSmallTalk = Random.nextDouble() < percent
|
||||
|
||||
if (!doSmallTalk) return TimeTrainingSuccess
|
||||
if (TimeQuestionTypes.isEmpty()) return TimeTrainingSuccess
|
||||
|
||||
val ctx = buildSmallTalkContextTime(question)
|
||||
val proxyQuestion = requestSmallTalk(ctx)
|
||||
|
||||
smallTalkUsed++
|
||||
|
||||
return if (proxyQuestion.isNotBlank()) {
|
||||
genericSmallTalk(
|
||||
context = ctx,
|
||||
nextState = TimeTrainingSuccess,
|
||||
preparedQuestion = proxyQuestion
|
||||
)
|
||||
} else {
|
||||
fallbackSmallTalk(question)
|
||||
}
|
||||
}
|
||||
|
||||
fun smallTalkAfterQuestion(question: TimeQuestionType): State {
|
||||
if (smallTalkUsed >= MAX_SMALLTALK_PER_SESSION) return TimeTrainingSuccess
|
||||
|
||||
// kolko otazok este su v liste
|
||||
val denom = TimeQuestionTypes.size + 1
|
||||
|
||||
val smallTalkLeft = MAX_SMALLTALK_PER_SESSION - smallTalkUsed
|
||||
|
||||
// výpočty šancov
|
||||
val percent = minOf(0.5, smallTalkLeft.toDouble() / denom.toDouble())
|
||||
val doSmallTalk = Random.nextDouble() < percent
|
||||
|
||||
if (!doSmallTalk) return TimeTrainingSuccess
|
||||
if (TimeQuestionTypes.isEmpty()) return TimeTrainingSuccess
|
||||
|
||||
smallTalkUsed++
|
||||
return when (question) {
|
||||
TimeQuestionType.CURRENT_TIME -> CurrentTimeSmallTalk
|
||||
TimeQuestionType.TODAY_DATE -> TodayDateSmallTalk
|
||||
TimeQuestionType.TOMORROW_DATE -> TomorrowDateSmallTalk
|
||||
TimeQuestionType.CURRENT_MONTH -> CurrentMonthSmallTalk
|
||||
TimeQuestionType.TODAY_WEEKDAY -> WeekdaySmallTalk
|
||||
TimeQuestionType.DAY_PERIOD -> DayPeriodSmallTalk
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------
|
||||
|
||||
// vráti náhodný ešte nepoužitý typ a zároveň ho odstráni zo zoznamu.
|
||||
fun pickRandomTimeQuestionType(): TimeQuestionType {
|
||||
// prioritne otazky -> potom ostatne
|
||||
while (priorityIndex < priorityTimeQuestionOrder.size) {
|
||||
val candidate = priorityTimeQuestionOrder[priorityIndex]
|
||||
priorityIndex++
|
||||
|
||||
if (TimeQuestionTypes.remove(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
val chosen = TimeQuestionTypes.random()
|
||||
TimeQuestionTypes.remove(chosen)
|
||||
return chosen
|
||||
}
|
||||
|
||||
fun resetTimeQuestions() {
|
||||
TimeQuestionTypes = mutableListOf(
|
||||
TimeQuestionType.CURRENT_TIME,
|
||||
TimeQuestionType.TODAY_DATE,
|
||||
TimeQuestionType.TOMORROW_DATE,
|
||||
TimeQuestionType.CURRENT_MONTH,
|
||||
TimeQuestionType.TODAY_WEEKDAY,
|
||||
TimeQuestionType.DAY_PERIOD
|
||||
)
|
||||
priorityIndex = 0
|
||||
}
|
||||
|
||||
private fun correctAnswerFor(question: TimeQuestionType): String = when (question) {
|
||||
TimeQuestionType.CURRENT_TIME -> DateTimeChecker.CorrectCurrentTime()
|
||||
TimeQuestionType.TODAY_DATE -> DateTimeChecker.CorrectTodayDate()
|
||||
TimeQuestionType.TOMORROW_DATE -> DateTimeChecker.CorrectTomorrowDate()
|
||||
TimeQuestionType.CURRENT_MONTH -> DateTimeChecker.CorrectCurrentMonth()
|
||||
TimeQuestionType.TODAY_WEEKDAY -> DateTimeChecker.CorrectTodayWeekday()
|
||||
TimeQuestionType.DAY_PERIOD -> DateTimeChecker.CorrectCurrentDayPeriod()
|
||||
}
|
||||
|
||||
// pre treti nespravny pokus
|
||||
fun FlowControlRunner.finalWrongAnswer() {
|
||||
val correct = correctAnswerFor(currentTimeQuestionType)
|
||||
|
||||
SessionLogger.log(
|
||||
game = "time",
|
||||
phase = "resolved",
|
||||
question = currentTimeQuestionType.name,
|
||||
attempt = wrongAttempts,
|
||||
result = "final_fail",
|
||||
correctAnswer = correct,
|
||||
userAnswer = "",
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)
|
||||
|
||||
lastWrongAttemptsForQuestion = wrongAttempts
|
||||
wrongAttempts = 0
|
||||
|
||||
when (currentTimeQuestionType) {
|
||||
TimeQuestionType.CURRENT_TIME -> {
|
||||
happyShake()
|
||||
furhat.say(
|
||||
"Dali ste nesprávnu odpoveď, ale nič sa nedeje! " +
|
||||
"Správna odpoveď by bola, že je teraz $correct."
|
||||
)
|
||||
delay(1000)
|
||||
}
|
||||
|
||||
TimeQuestionType.TODAY_DATE -> {
|
||||
happyShake()
|
||||
furhat.say(
|
||||
"Nevyšlo Vám to, ale vôbec to neprekáža. " +
|
||||
"Správna odpoveď by bola, že dnes je $correct. " +
|
||||
"Teraz si to pamätáte!"
|
||||
)
|
||||
delay(1000)
|
||||
}
|
||||
|
||||
TimeQuestionType.TOMORROW_DATE -> {
|
||||
empathy()
|
||||
furhat.say(
|
||||
"Vyzerá to, že je to dnes náročné, ale to je v poriadku " +
|
||||
"Správna odpoveď by bola, že zajtra bude $correct. "
|
||||
)
|
||||
furhat.say("Teraz si to pamätáte!")
|
||||
delay(1000)
|
||||
}
|
||||
|
||||
TimeQuestionType.CURRENT_MONTH -> {
|
||||
empathy()
|
||||
furhat.say(
|
||||
"Nechajte to tak, dnes je to náročné. " +
|
||||
"Správna odpoveď by bola, že je teraz mesiac $correct. "
|
||||
)
|
||||
delay(1000)
|
||||
}
|
||||
|
||||
TimeQuestionType.TODAY_WEEKDAY -> {
|
||||
calm()
|
||||
furhat.say(
|
||||
"Tentoraz to nevyšlo, ale nevadí. " +
|
||||
"Správna odpoveď by bola: $correct."
|
||||
)
|
||||
delay(1000)
|
||||
}
|
||||
|
||||
TimeQuestionType.DAY_PERIOD -> {
|
||||
happyShake()
|
||||
furhat.say(
|
||||
"Dali ste nesprávnu odpoveď, ale nič sa nedeje! " +
|
||||
"Správna odpoveď by bola, že je teraz $correct."
|
||||
)
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
goto(TimeTrainingSuccess)
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------
|
||||
// Pomocne premenne
|
||||
//--------------------------------------------------------------
|
||||
|
||||
var currentTimeQuestionType: TimeQuestionType = TimeQuestionType.CURRENT_TIME
|
||||
var questionsSinceLastCheck = 0
|
||||
var questionText: String = ""
|
||||
|
||||
var wrongAttempts = 0
|
||||
var lastWrongAttemptsForQuestion = 0
|
||||
|
||||
var hintUsedForCurrentQuestion: Boolean = false
|
||||
|
||||
var lastDayPeriod: String = ""
|
||||
|
||||
//==============================================================
|
||||
// Proces hry
|
||||
//==============================================================
|
||||
|
||||
// Intro
|
||||
val TimeTrainingIntro: State = state(Parent) {
|
||||
onEntry {
|
||||
wrongAttempts = 0
|
||||
smallTalkUsed = 0
|
||||
questionsSinceLastCheck = 0
|
||||
resetTimeQuestions() //ранее не было
|
||||
hintUsedForCurrentQuestion = false
|
||||
|
||||
currentTimeQuestionType = pickRandomTimeQuestionType()
|
||||
goto(AskTime)
|
||||
}
|
||||
}
|
||||
|
||||
// AskTime: otazky a reakcia na vysledok
|
||||
val AskTime: State = state(Parent) {
|
||||
onEntry {
|
||||
questionText = when (currentTimeQuestionType) {
|
||||
TimeQuestionType.CURRENT_TIME -> "Môžete mi, prosím, povedať, koľko je teraz hodín?"
|
||||
TimeQuestionType.TODAY_DATE -> "Viete mi povedať, aký je dnes dátum?"
|
||||
TimeQuestionType.TOMORROW_DATE -> "Aký dátum bude zajtra?"
|
||||
TimeQuestionType.CURRENT_MONTH -> "Viete, aký je práve mesiac?"
|
||||
TimeQuestionType.TODAY_WEEKDAY -> "Aký je dnes deň v týždni?"
|
||||
TimeQuestionType.DAY_PERIOD -> "Povedzte, prosím, aká je teraz približne denná doba?"
|
||||
}
|
||||
askRepeatable(questionText)
|
||||
|
||||
SessionLogger.log(
|
||||
game = "time",
|
||||
phase = "question_shown",
|
||||
question = currentTimeQuestionType.name,
|
||||
attempt = wrongAttempts + 1,
|
||||
result = "asked",
|
||||
correctAnswer = correctAnswerFor(currentTimeQuestionType),
|
||||
userAnswer = "",
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)
|
||||
}
|
||||
|
||||
onResponse<DontKnow> {
|
||||
|
||||
SessionLogger.log(
|
||||
game = "time",
|
||||
phase = "answer",
|
||||
question = currentTimeQuestionType.name,
|
||||
attempt = wrongAttempts + 1,
|
||||
result = "dont_know",
|
||||
correctAnswer = correctAnswerFor(currentTimeQuestionType),
|
||||
userAnswer = it.text ?: "",
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)
|
||||
|
||||
furhat.say(pendingFileStatusMessage())
|
||||
|
||||
empathy()
|
||||
furhat.say("Rozumiem, to je v poriadku.")
|
||||
|
||||
// este nebola napoveda
|
||||
if (!hintUsedForCurrentQuestion) {
|
||||
if (wrongAttempts < 2) wrongAttempts = 2
|
||||
goto(TimeHintOffer)
|
||||
return@onResponse
|
||||
}
|
||||
|
||||
wrongAttempts = 3
|
||||
finalWrongAnswer()
|
||||
}
|
||||
|
||||
onResponse<Rephrase> {
|
||||
handleRephrase(it.intent)
|
||||
}
|
||||
|
||||
onResponse<Help> {
|
||||
|
||||
SessionLogger.log(
|
||||
game = "time",
|
||||
phase = "event",
|
||||
question = currentTimeQuestionType.name,
|
||||
attempt = wrongAttempts + 1,
|
||||
result = "help_requested",
|
||||
correctAnswer = correctAnswerFor(currentTimeQuestionType),
|
||||
userAnswer = it.text ?: "",
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)
|
||||
|
||||
if (!hintUsedForCurrentQuestion) {
|
||||
veryHappy()
|
||||
furhat.say("Samozrejme, pomôžem Vám! Dajte mi chvíľu, prosím")
|
||||
if (wrongAttempts < 2) wrongAttempts = 2
|
||||
goto(TimeHint)
|
||||
return@onResponse
|
||||
}
|
||||
|
||||
empathy()
|
||||
furhat.say("Prepáčte, už som Vám dala nápovedu.")
|
||||
delay(700)
|
||||
|
||||
littleSad()
|
||||
furhat.say("Je mi ľúto, že Vám nepomohla. Skúste odpovedať podľa toho, čo si pamätáte.")
|
||||
|
||||
val questionText = when (currentTimeQuestionType) {
|
||||
TimeQuestionType.CURRENT_TIME ->
|
||||
"Môžete mi ešte raz povedať, koľko je teraz približne hodín?"
|
||||
|
||||
TimeQuestionType.TODAY_DATE ->
|
||||
"Skúste mi ešte raz povedať, aký je dnes dátum."
|
||||
|
||||
TimeQuestionType.TOMORROW_DATE ->
|
||||
"Skúste mi ešte raz povedať, aký dátum bude zajtra."
|
||||
|
||||
TimeQuestionType.CURRENT_MONTH ->
|
||||
"Skúste mi, prosím, ešte raz povedať, aký je teraz mesiac."
|
||||
|
||||
TimeQuestionType.TODAY_WEEKDAY ->
|
||||
"Skúste mi ešte raz povedať, aký je dnes deň v týždni."
|
||||
|
||||
TimeQuestionType.DAY_PERIOD ->
|
||||
"Skúste mi, prosím, ešte raz povedať, či je teraz ráno, deň alebo večer."
|
||||
}
|
||||
|
||||
askRepeatable(questionText)
|
||||
}
|
||||
|
||||
onResponse<StopTraining> {
|
||||
if (handleStop(it.intent,
|
||||
"time",
|
||||
currentTimeQuestionType.name,
|
||||
it.text ?: ""))
|
||||
{
|
||||
TrainingMenuFlags.hasTrainedOnce = true
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
return@onResponse
|
||||
}
|
||||
}
|
||||
|
||||
onResponse<Repeat>{
|
||||
if (handleRepeat(
|
||||
it.intent,
|
||||
"time",
|
||||
currentTimeQuestionType.name,
|
||||
correctAnswerFor(currentTimeQuestionType),
|
||||
it.text ?: "",
|
||||
hintUsedForCurrentQuestion))
|
||||
{
|
||||
return@onResponse
|
||||
}
|
||||
}
|
||||
|
||||
onResponse {
|
||||
val text = it.text ?: ""
|
||||
|
||||
when (currentTimeQuestionType) {
|
||||
TimeQuestionType.CURRENT_TIME -> {
|
||||
val success = DateTimeChecker.isCorrectCurrentTime(text)
|
||||
val correct = correctAnswerFor(currentTimeQuestionType)
|
||||
|
||||
SessionLogger.log(
|
||||
game = "time",
|
||||
phase = "answer",
|
||||
question = currentTimeQuestionType.name,
|
||||
attempt = wrongAttempts + 1,
|
||||
result = if (success) "success" else "fail",
|
||||
correctAnswer = correct,
|
||||
userAnswer = text,
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)
|
||||
|
||||
if (success) {
|
||||
lastWrongAttemptsForQuestion = wrongAttempts
|
||||
wrongAttempts = 0
|
||||
veryHappy()
|
||||
furhat.say("Správne! Povedali ste aktuálny čas. ")
|
||||
goto(smallTalkManagerTime(currentTimeQuestionType))
|
||||
} else {
|
||||
wrongAttempts++
|
||||
when (wrongAttempts) {
|
||||
1 -> {
|
||||
littleSad()
|
||||
furhat.say(
|
||||
"Nezdá sa, že to bol aktuálny čas. " +
|
||||
"Skúste ho povedať napríklad ako „je päť hodín“ alebo „je 5:00“."
|
||||
)
|
||||
delay(700)
|
||||
reentry()
|
||||
}
|
||||
|
||||
2 -> {
|
||||
// val correct = correctAnswerFor(currentTimeQuestionType)
|
||||
explainWhyWrong(
|
||||
question = questionText,
|
||||
correctAnswer = correct,
|
||||
userAnswer = text,
|
||||
attempt = wrongAttempts
|
||||
)
|
||||
goto(TimeHintOffer)
|
||||
}
|
||||
|
||||
else -> {
|
||||
finalWrongAnswer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if (DateTimeChecker.isCorrectCurrentTime(text)) {
|
||||
// lastWrongAttemptsForQuestion = wrongAttempts
|
||||
// wrongAttempts = 0
|
||||
// veryHappy()
|
||||
// furhat.say("Správne! Povedali ste aktuálny čas. ")
|
||||
// goto(smallTalkManagerTime(currentTimeQuestionType))
|
||||
// } else {
|
||||
// wrongAttempts++
|
||||
// when (wrongAttempts) {
|
||||
// 1 -> {
|
||||
// littleSad()
|
||||
// furhat.say(
|
||||
// "Nezdá sa, že to bol aktuálny čas. " +
|
||||
// "Skúste ho povedať napríklad ako „je päť hodín“ alebo „je 5:00“."
|
||||
// )
|
||||
// delay(700)
|
||||
// reentry()
|
||||
// }
|
||||
//
|
||||
// 2 -> {
|
||||
//// val correct = correctAnswerFor(currentTimeQuestionType)
|
||||
// explainWhyWrong(
|
||||
// question = questionText,
|
||||
// correctAnswer = correct,
|
||||
// userAnswer = text,
|
||||
// attempt = wrongAttempts
|
||||
// )
|
||||
// goto(TimeHintOffer)
|
||||
// }
|
||||
//
|
||||
// else -> {
|
||||
// finalWrongAnswer()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
TimeQuestionType.TODAY_DATE -> {
|
||||
val success = DateTimeChecker.isCorrectTodayDate(text)
|
||||
val correct = correctAnswerFor(currentTimeQuestionType)
|
||||
|
||||
SessionLogger.log(
|
||||
game = "time",
|
||||
phase = "answer",
|
||||
question = currentTimeQuestionType.name,
|
||||
attempt = wrongAttempts + 1,
|
||||
result = if (success) "success" else "fail",
|
||||
correctAnswer = correct,
|
||||
userAnswer = text,
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)
|
||||
|
||||
if (success) {
|
||||
lastWrongAttemptsForQuestion = wrongAttempts
|
||||
wrongAttempts = 0
|
||||
veryHappy()
|
||||
furhat.say("Správne, dnes je takýto dátum.")
|
||||
goto(smallTalkManagerTime(currentTimeQuestionType))
|
||||
} else {
|
||||
wrongAttempts++
|
||||
when (wrongAttempts) {
|
||||
1 -> {
|
||||
littleSad()
|
||||
furhat.say(
|
||||
"Zdá sa, že to nie je dnešný dátum. " +
|
||||
"Skúste ho povedať ešte raz, napríklad \"dnes je dvadsiatý decembr\"."
|
||||
)
|
||||
delay(700)
|
||||
reentry()
|
||||
}
|
||||
2 -> {
|
||||
val correct = correctAnswerFor(currentTimeQuestionType)
|
||||
explainWhyWrong(
|
||||
question = questionText,
|
||||
correctAnswer = correct,
|
||||
userAnswer = text,
|
||||
attempt = wrongAttempts
|
||||
)
|
||||
goto(TimeHintOffer)
|
||||
}
|
||||
else -> {
|
||||
finalWrongAnswer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TimeQuestionType.TOMORROW_DATE -> {
|
||||
val success = DateTimeChecker.isCorrectTomorrowDate(text)
|
||||
val correct = correctAnswerFor(currentTimeQuestionType)
|
||||
|
||||
SessionLogger.log(
|
||||
game = "time",
|
||||
phase = "answer",
|
||||
question = currentTimeQuestionType.name,
|
||||
attempt = wrongAttempts + 1,
|
||||
result = if (success) "success" else "fail",
|
||||
correctAnswer = correct,
|
||||
userAnswer = text,
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)
|
||||
|
||||
if (success) {
|
||||
lastWrongAttemptsForQuestion = wrongAttempts
|
||||
wrongAttempts = 0
|
||||
veryHappy()
|
||||
furhat.say("Správne! To je zajtrajší dátum.")
|
||||
goto(smallTalkManagerTime(currentTimeQuestionType))
|
||||
} else {
|
||||
wrongAttempts++
|
||||
when (wrongAttempts) {
|
||||
1 -> {
|
||||
littleSad()
|
||||
furhat.say(
|
||||
"Nie som si istá, že to je zajtrajší dátum. " +
|
||||
"Skúste ho povedať ešte raz."
|
||||
)
|
||||
delay(700)
|
||||
reentry()
|
||||
}
|
||||
2 -> {
|
||||
val correct = correctAnswerFor(currentTimeQuestionType)
|
||||
explainWhyWrong(
|
||||
question = questionText,
|
||||
correctAnswer = correct,
|
||||
userAnswer = text,
|
||||
attempt = wrongAttempts
|
||||
)
|
||||
goto(TimeHintOffer)
|
||||
}
|
||||
else -> {
|
||||
finalWrongAnswer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TimeQuestionType.CURRENT_MONTH -> {
|
||||
val success = DateTimeChecker.isCorrectCurrentMonth(text)
|
||||
val correct = correctAnswerFor(currentTimeQuestionType)
|
||||
|
||||
SessionLogger.log(
|
||||
game = "time",
|
||||
phase = "answer",
|
||||
question = currentTimeQuestionType.name,
|
||||
attempt = wrongAttempts + 1,
|
||||
result = if (success) "success" else "fail",
|
||||
correctAnswer = correct,
|
||||
userAnswer = text,
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)
|
||||
|
||||
if (success) {
|
||||
lastWrongAttemptsForQuestion = wrongAttempts
|
||||
wrongAttempts = 0
|
||||
veryHappy()
|
||||
furhat.say("Správne, je to aktuálny mesiac.")
|
||||
goto(smallTalkManagerTime(currentTimeQuestionType))
|
||||
} else {
|
||||
wrongAttempts++
|
||||
when (wrongAttempts) {
|
||||
1 -> {
|
||||
littleSad()
|
||||
furhat.say(
|
||||
"Nezdá sa, že to bol aktuálny mesiac. " +
|
||||
"Skúste ho povedať ešte raz."
|
||||
)
|
||||
delay(700)
|
||||
reentry()
|
||||
}
|
||||
2 -> {
|
||||
val correct = correctAnswerFor(currentTimeQuestionType)
|
||||
explainWhyWrong(
|
||||
question = questionText,
|
||||
correctAnswer = correct,
|
||||
userAnswer = text,
|
||||
attempt = wrongAttempts
|
||||
)
|
||||
goto(TimeHintOffer)
|
||||
}
|
||||
else -> {
|
||||
finalWrongAnswer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TimeQuestionType.TODAY_WEEKDAY -> {
|
||||
val success = DateTimeChecker.isCorrectTodayWeekday(text)
|
||||
val correct = correctAnswerFor(currentTimeQuestionType)
|
||||
|
||||
SessionLogger.log(
|
||||
game = "time",
|
||||
phase = "answer",
|
||||
question = currentTimeQuestionType.name,
|
||||
attempt = wrongAttempts + 1,
|
||||
result = if (success) "success" else "fail",
|
||||
correctAnswer = correct,
|
||||
userAnswer = text,
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)
|
||||
|
||||
if (success) {
|
||||
lastWrongAttemptsForQuestion = wrongAttempts
|
||||
wrongAttempts = 0
|
||||
veryHappy()
|
||||
furhat.say("Správne! Povedali ste dnešný deň v týždni.")
|
||||
goto(smallTalkManagerTime(currentTimeQuestionType))
|
||||
} else {
|
||||
wrongAttempts++
|
||||
when (wrongAttempts) {
|
||||
1 -> {
|
||||
littleSad()
|
||||
furhat.say(
|
||||
"Nezdá sa, že to bol správny deň v týždni. " +
|
||||
"Skúste to povedať inak."
|
||||
)
|
||||
delay(700)
|
||||
reentry()
|
||||
}
|
||||
2 -> {
|
||||
val correct = correctAnswerFor(currentTimeQuestionType)
|
||||
explainWhyWrong(
|
||||
question = questionText,
|
||||
correctAnswer = correct,
|
||||
userAnswer = text,
|
||||
attempt = wrongAttempts
|
||||
)
|
||||
goto(TimeHintOffer)
|
||||
}
|
||||
else -> {
|
||||
finalWrongAnswer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TimeQuestionType.DAY_PERIOD -> {
|
||||
val success = DateTimeChecker.isCorrectCurrentDayPeriod(text)
|
||||
val correct = correctAnswerFor(currentTimeQuestionType)
|
||||
|
||||
SessionLogger.log(
|
||||
game = "time",
|
||||
phase = "answer",
|
||||
question = currentTimeQuestionType.name,
|
||||
attempt = wrongAttempts + 1,
|
||||
result = if (success) "success" else "fail",
|
||||
correctAnswer = correct,
|
||||
userAnswer = text,
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)
|
||||
|
||||
if (success) {
|
||||
lastWrongAttemptsForQuestion = wrongAttempts
|
||||
wrongAttempts = 0
|
||||
veryHappy()
|
||||
furhat.say("Správne! Povedali ste dnešný deň v týždni.")
|
||||
goto(smallTalkManagerTime(currentTimeQuestionType))
|
||||
} else {
|
||||
wrongAttempts++
|
||||
|
||||
when (wrongAttempts) {
|
||||
1 -> {
|
||||
littleSad()
|
||||
furhat.say(
|
||||
"Nie som si istá, že to sedí s aktuálnou dennou dobou. " +
|
||||
"Skúste povedať, či je teraz skôr ráno, deň alebo večer."
|
||||
)
|
||||
delay(700)
|
||||
reentry()
|
||||
}
|
||||
|
||||
2 -> {
|
||||
val correct = correctAnswerFor(currentTimeQuestionType)
|
||||
explainWhyWrong(
|
||||
question = questionText,
|
||||
correctAnswer = correct,
|
||||
userAnswer = text,
|
||||
attempt = wrongAttempts
|
||||
)
|
||||
goto(TimeHintOffer)
|
||||
}
|
||||
|
||||
else -> {
|
||||
finalWrongAnswer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onNoResponse {
|
||||
SessionLogger.log(
|
||||
game = "time",
|
||||
phase = "answer",
|
||||
question = currentTimeQuestionType.name,
|
||||
attempt = wrongAttempts + 1,
|
||||
result = "no_response",
|
||||
correctAnswer = correctAnswerFor(currentTimeQuestionType),
|
||||
userAnswer = "",
|
||||
hintUsed = hintUsedForCurrentQuestion
|
||||
)
|
||||
|
||||
when {
|
||||
wrongAttempts < 2 -> {
|
||||
wrongAttempts = 2
|
||||
goto(TimeHintOffer)
|
||||
}
|
||||
|
||||
// napoveda uz bola -> znova nespravny pokus -> hovori odpoved'
|
||||
else -> {
|
||||
val correct = correctAnswerFor(currentTimeQuestionType)
|
||||
|
||||
lastWrongAttemptsForQuestion = 3
|
||||
wrongAttempts = 0
|
||||
|
||||
empathy()
|
||||
furhat.say(
|
||||
"Zdá sa, že je to teraz pre Vás náročné, ale to je v poriadku. " +
|
||||
"Správna odpoveď na otázku je $correct."
|
||||
)
|
||||
delay(1000)
|
||||
goto(TimeTrainingSuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val TimeTrainingSuccess: State = state(Parent) {
|
||||
onEntry {
|
||||
when (lastWrongAttemptsForQuestion) {
|
||||
1 -> {
|
||||
veryHappy()
|
||||
furhat.say("Vy ste šikovný!")
|
||||
delay(1000)
|
||||
}
|
||||
2 -> {
|
||||
calm()
|
||||
furhat.say("Aj keď Vám to zabralo čas, zvládli ste to dobre.")
|
||||
delay(1000)
|
||||
}
|
||||
3 -> {
|
||||
empathy()
|
||||
furhat.say("Nezvladli ste to, ale to je v pohode. Teraz je to pre Vás náročné, ale nezúfajte, časom si to osvojíte.")
|
||||
delay(1000)
|
||||
}
|
||||
|
||||
else -> {
|
||||
veryHappy()
|
||||
furhat.say("Zvládli ste to veľmi dobre.")
|
||||
}
|
||||
}
|
||||
|
||||
if (TimeQuestionTypes.isEmpty()) {
|
||||
calm()
|
||||
furhat.say("Týmto sme ukončili hru s časom.")
|
||||
veryHappy()
|
||||
furhat.say("Ďakujem, bolo to výborné.")
|
||||
delay(1000)
|
||||
|
||||
TrainingMenuFlags.allTimeQuestionsCompleted = true
|
||||
TrainingMenuFlags.hasTrainedOnce = true
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
}
|
||||
|
||||
questionsSinceLastCheck++
|
||||
|
||||
val continueOffer = questionsSinceLastCheck >= 2
|
||||
|
||||
if (continueOffer) {
|
||||
val wantsToContinue = call(AskToContinue()) as Boolean
|
||||
if (!wantsToContinue) {
|
||||
TrainingMenuFlags.hasTrainedOnce = true
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
return@onEntry
|
||||
}
|
||||
questionsSinceLastCheck = 0
|
||||
}
|
||||
wrongAttempts = 0
|
||||
hintUsedForCurrentQuestion = false
|
||||
|
||||
currentTimeQuestionType = pickRandomTimeQuestionType()
|
||||
goto(AskTime)
|
||||
}
|
||||
}
|
||||
158
src/main/kotlin/furhatos/app/blank/flow/main/say_time/STT.kt
Normal file
158
src/main/kotlin/furhatos/app/blank/flow/main/say_time/STT.kt
Normal file
@ -0,0 +1,158 @@
|
||||
package furhatos.app.blank.flow.main.supporting
|
||||
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.flow.main.StartQuestion
|
||||
import furhatos.app.blank.flow.main.handlers.Goodbye
|
||||
import furhatos.app.blank.flow.main.handlers.askRepeatable
|
||||
import furhatos.app.blank.flow.main.handlers.handleRepeat
|
||||
import furhatos.app.blank.flow.main.handlers.handleRephrase
|
||||
import furhatos.app.blank.flow.main.handlers.handleStop
|
||||
import furhatos.app.blank.flow.main.say_time.DateTimeChecker
|
||||
import furhatos.app.blank.flow.main.say_time.DayPeriod
|
||||
import furhatos.app.blank.flow.main.say_time.TimeTrainingSuccess
|
||||
import furhatos.app.blank.flow.main.say_time.currentTimeQuestionType
|
||||
import furhatos.app.blank.flow.main.say_time.lastDayPeriod
|
||||
import furhatos.app.blank.flow.main.supporting.general.TrainingMenuFlags
|
||||
import furhatos.flow.kotlin.*
|
||||
import furhatos.app.blank.nlu.base_answer.Ano
|
||||
import furhatos.app.blank.nlu.base_answer.Nie
|
||||
import furhatos.app.blank.nlu.base_answer.Rephrase
|
||||
import furhatos.app.blank.nlu.base_answer.StopTraining
|
||||
|
||||
val DayPeriodSmallTalk: State = state(Parent) {
|
||||
|
||||
onEntry {
|
||||
val question4 = when (lastDayPeriod.lowercase()) {
|
||||
"ráno" -> "Povedali ste, že je teraz ráno. Na dobré ráno patria chutné raňajky – už ste raňajkovali?"
|
||||
"deň" -> "Povedali ste, že je teraz deň. Počas dňa sa zíde mať plán – máte dnes niečo naplánované?"
|
||||
else-> "Povedali ste, že je teraz večer. Večer je dobrý na oddych – už ste si dnes trochu oddýchli?"
|
||||
}
|
||||
|
||||
askRepeatable(question4)
|
||||
}
|
||||
|
||||
onResponse<Ano> {
|
||||
furhat.say("To je výborne. Pokračujme ďalej.")
|
||||
goto(TimeTrainingSuccess)
|
||||
}
|
||||
|
||||
onResponse<Nie> {
|
||||
furhat.say("To nevadí. Dôležité je, že sa snažíte. Pokračujme ďalej.")
|
||||
goto(TimeTrainingSuccess)
|
||||
}
|
||||
|
||||
onResponse<Rephrase> {
|
||||
handleRephrase(it.intent)
|
||||
}
|
||||
|
||||
onResponse<StopTraining> {
|
||||
if (handleStop(it.intent,
|
||||
"time",
|
||||
currentTimeQuestionType.name,
|
||||
it.text ?: ""))
|
||||
{
|
||||
TrainingMenuFlags.hasTrainedOnce = true
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
return@onResponse
|
||||
}
|
||||
}
|
||||
|
||||
onResponse {
|
||||
if (handleStop(it.intent, "time", "small_talk", it.text ?: "")){
|
||||
TrainingMenuFlags.hasTrainedOnce = true
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
return@onResponse
|
||||
}
|
||||
// if (handleRepeat(it.intent)){
|
||||
// return@onResponse
|
||||
// }
|
||||
|
||||
furhat.ask("Prepáčte, stačí povedať áno alebo nie.")
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- CURRENT TIME --------------------
|
||||
|
||||
val CurrentTimeSmallTalk: State = SmallTalk {
|
||||
val period = DateTimeChecker.currentDayPeriod()
|
||||
val questionHodiny = when (period) {
|
||||
DayPeriod.MORNING -> "Ešte je ráno, máte celý deň pred sebou. Máte dnes ešte niečo, čo chcete stihnúť?"
|
||||
DayPeriod.DAY -> "Je ešte deň. Máte dnes ešte niečo, čo chcete stihnúť?"
|
||||
DayPeriod.EVENING -> "Už je večer. Máte ešte dnes niečo, čo by ste chceli stihnúť?"
|
||||
}
|
||||
|
||||
listOf(
|
||||
"Ste skôr ranný typ, alebo nočná sova?",
|
||||
questionHodiny
|
||||
).random()
|
||||
}
|
||||
|
||||
// -------------------- TODAY DATE --------------------
|
||||
|
||||
val TodayDateSmallTalk: State = SmallTalk {
|
||||
listOf(
|
||||
"Máte dnes nejakú drobnosť, na ktorú sa tešíte?",
|
||||
"Viete si spomenúť, či je dnes niečí sviatok alebo meniny vo vašom okolí?",
|
||||
"Máte radšej začiatok mesiaca, alebo jeho koniec?"
|
||||
).random()
|
||||
}
|
||||
|
||||
// -------------------- TOMORROW DATE --------------------
|
||||
|
||||
val TomorrowDateSmallTalk: State = SmallTalk {
|
||||
listOf(
|
||||
"Máte zajtra niečo naplánované?",
|
||||
"Tešíte sa viac na zajtrajšok, alebo ste spokojní s dneškom?",
|
||||
"Zajtra je nový deň — chcete si zajtra niečo dopriať alebo urobiť inak?",
|
||||
"Čo by vám zajtra urobilo radosť, aj keby to bola maličkosť?"
|
||||
).random()
|
||||
}
|
||||
|
||||
// -------------------- CURRENT MONTH --------------------
|
||||
|
||||
val CurrentMonthSmallTalk: State = SmallTalk {
|
||||
listOf(
|
||||
"Čím si ho najviac spájate — počasím, sviatkami, alebo niečím iným?",
|
||||
"Máte v tomto mesiaci niečo, na čo sa radi pripravujete?",
|
||||
"Viete, čo máte na tomto mesiaci najradšej?",
|
||||
"Je tento mesiac pre vás skôr pokojný, alebo máte veľa povinností?"
|
||||
).random()
|
||||
}
|
||||
|
||||
//------------------------------
|
||||
private fun weekdayInPhrase(day: String): String = when (day.lowercase()) {
|
||||
"pondelok" -> "v pondelok"
|
||||
"utorok" -> "v utorok"
|
||||
"streda" -> "v stredu"
|
||||
"štvrtok" -> "vo štvrtok"
|
||||
"piatok" -> "v piatok"
|
||||
"sobota" -> "v sobotu"
|
||||
"nedeľa", "nedela" -> "v nedeľu"
|
||||
else -> "dnes"
|
||||
}
|
||||
|
||||
val WeekdaySmallTalk: State = SmallTalk {
|
||||
val day = DateTimeChecker.CorrectTodayWeekday()
|
||||
val dayLc = day.lowercase()
|
||||
val dayPrep = weekdayInPhrase(day)
|
||||
|
||||
val daySpecific = when (dayLc) {
|
||||
"pondelok" -> "Povedali ste, že je dnes pondelok. Pondelok je často štart týždňa – máte chuť začať deň pomalšie, alebo hneď naplno?"
|
||||
"utorok" -> "Povedali ste, že je dnes utorok. Utorok býva taký pracovný rozbeh – máte dnes niečo, čo chcete vybaviť?"
|
||||
"streda" -> "Povedali ste, že je dnes streda. Streda je polovica týždňa – máte pocit, že týždeň ide rýchlo, alebo pomaly?"
|
||||
"štvrtok" -> "Povedali ste, že je dnes štvrtok. Už sa blíži víkend – tešíte sa na niečo v najbližších dňoch?"
|
||||
"piatok" -> "Povedali ste, že je dnes piatok. Piatok znie príjemne – plánujete si dnes dopriať trochu oddychu?"
|
||||
"sobota" -> "Povedali ste, že je dnes sobota. Sobota je často na oddych – máte dnes niečo príjemné v pláne?"
|
||||
"nedeľa", "nedela" -> "Povedali ste, že je dnes nedeľa. Nedeľa býva pokojná – ako najradšej trávite nedeľu?"
|
||||
else -> "Ďakujem. A aký máte dnes deň?"
|
||||
}
|
||||
|
||||
val universal = listOf(
|
||||
"Je pre vás $day skôr \"pracovný\" alebo \"oddychový\"?",
|
||||
"Máte $dayPrep nejaký malý zvyk alebo rutinu?",
|
||||
"Chcete si dnes radšej naplánovať niečo, alebo nechať deň plynúť voľne?",
|
||||
"Keby ste si mali vybrať jednu vec na dnes, čo by to bolo?"
|
||||
)
|
||||
|
||||
listOf(daySpecific, universal.random()).random()
|
||||
}
|
||||
@ -0,0 +1,358 @@
|
||||
package furhatos.app.blank.flow.main.say_time
|
||||
|
||||
import java.time.*
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
enum class TimeQuestionType {
|
||||
CURRENT_TIME,
|
||||
TODAY_DATE,
|
||||
TOMORROW_DATE,
|
||||
CURRENT_MONTH,
|
||||
TODAY_WEEKDAY,
|
||||
DAY_PERIOD
|
||||
}
|
||||
|
||||
enum class DayPeriod {
|
||||
MORNING,
|
||||
DAY,
|
||||
EVENING
|
||||
}
|
||||
|
||||
|
||||
object DateTimeChecker {
|
||||
|
||||
private fun now(): LocalDateTime = LocalDateTime.now()
|
||||
|
||||
// ---------- 'slovo -> cislo' pre mesiac a hodiny ----------
|
||||
|
||||
private val NUMBER_WORDS = mapOf(
|
||||
"nula" to 0, "nultý" to 0,
|
||||
"jeden" to 1, "jedna" to 1, "jedno" to 1, "prvý" to 1, "prvá" to 1,
|
||||
"dva" to 2, "dve" to 2, "druhý" to 2, "druhá" to 2,
|
||||
"tri" to 3, "tretí" to 3, "tretia" to 3,
|
||||
"štyri" to 4, "styri" to 4, "štvrtý" to 4, "štvrtá" to 4,
|
||||
"päť" to 5, "pat" to 5, "piaty" to 5, "piata" to 5,
|
||||
"šesť" to 6, "sest" to 6, "šiesty" to 6, "šiesta" to 6,
|
||||
"sedem" to 7, "siedmy" to 7, "siedma" to 7,
|
||||
"osem" to 8, "ôsmy" to 8, "ôsma" to 8,
|
||||
"deväť" to 9, "devat" to 9, "deviaty" to 9, "deviatá" to 9,
|
||||
|
||||
"desať" to 10, "desat" to 10, "desiaty" to 10, "desiatá" to 10,
|
||||
"jedenásť" to 11, "jedenast" to 11, "jedenásty" to 11, "jedenásta" to 11,
|
||||
"dvanásť" to 12, "dvanast" to 12, "dvanásty" to 12, "dvanásta" to 12,
|
||||
"trinásť" to 13, "trinast" to 13, "trinásty" to 13, "trinásta" to 13,
|
||||
"štrnásť" to 14, "strnast" to 14, "štrnact" to 14, "štrnásty" to 14, "štrnásta" to 14,
|
||||
"pätnásť" to 15, "patnast" to 15, "pätnásty" to 15, "pätnásta" to 15,
|
||||
"šestnásť" to 16, "sestnast" to 16, "šestnásty" to 16, "šestnásta" to 16,
|
||||
"sedemnásť" to 17, "sedemnast" to 17, "sedemnásty" to 17, "sedemnástá" to 17,
|
||||
"osemnásť" to 18, "osemnast" to 18, "osemnásty" to 18, "osemnásta" to 18,
|
||||
"devätnásť" to 19, "devatnast" to 19, "devätnásty" to 19, "devätnásta" to 19,
|
||||
|
||||
"dvadsať" to 20, "dvadsat" to 20, "dvadsiatá" to 20,
|
||||
"dvadsaťjeden" to 21, "dvadsať jedna" to 21, "dvadsiaty prvý" to 21, "dvadsiať prvá" to 21,
|
||||
"dvadsaťdva" to 22, "dvadsať dva" to 22, "dvadsiaty druhý" to 22, "dvadsiať druhá" to 22,
|
||||
"dvadsaťtri" to 23, "dvadsať tri" to 23, "dvadsiaty tretí" to 23, "dvadsiať treťa" to 23
|
||||
)
|
||||
|
||||
private fun numberFromWord(raw: String): Int? {
|
||||
val cleaned = raw
|
||||
.trim('.', ',', ';')
|
||||
.lowercase()
|
||||
return NUMBER_WORDS[cleaned]
|
||||
}
|
||||
|
||||
// -------------- HODINY ----------------------------------
|
||||
fun parseClockTime(text: String): LocalTime? {
|
||||
val regex = Regex("""\b(\d{1,2})[ ](\d{1,2})\b""")
|
||||
val match = regex.find(text) ?: return null
|
||||
|
||||
val (hStr, mStr) = match.destructured
|
||||
val hour = hStr.toIntOrNull() ?: return null
|
||||
val minute = mStr.toIntOrNull() ?: return null
|
||||
|
||||
if (hour !in 0..23 || minute !in 0..59) return null
|
||||
|
||||
return LocalTime.of(hour, minute)
|
||||
}
|
||||
// -------------- HODINY (len cas ) ----------------------------------
|
||||
private fun parseHour(text: String): Int? {
|
||||
|
||||
val digitRegex = Regex("""\b(\d{1,2})\b""")
|
||||
val digitMatch = digitRegex.find(text)
|
||||
val hDigit = digitMatch?.groupValues?.get(1)?.toIntOrNull()
|
||||
if (hDigit != null && hDigit in 0..23) {
|
||||
return hDigit
|
||||
}
|
||||
|
||||
val wordRegex = Regex("""\b([\p{L}]+)\b""")
|
||||
for (m in wordRegex.findAll(text.lowercase())) {
|
||||
val word = m.groupValues[1]
|
||||
val num = numberFromWord(word) ?: continue
|
||||
if (num in 0..23) {
|
||||
return num
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun isCorrectCurrentTime(
|
||||
text: String,
|
||||
toleranceMinutes: Long = 10,
|
||||
hourToleranceHours: Int = 1 // допуск по часам для "примерного" ответа
|
||||
): Boolean {
|
||||
val nowTime = now().toLocalTime()
|
||||
|
||||
// ak presny cas
|
||||
val detailed = parseClockTime(text)
|
||||
if (detailed != null) {
|
||||
val diffMin = Duration.between(detailed, nowTime).abs().toMinutes()
|
||||
return diffMin <= toleranceMinutes
|
||||
}
|
||||
|
||||
// ak len hodina
|
||||
val hourOnly = parseHour(text) ?: return false
|
||||
val diffHours = kotlin.math.abs(hourOnly - nowTime.hour)
|
||||
return diffHours <= hourToleranceHours
|
||||
}
|
||||
|
||||
// -------------- DATUM ----------------------------
|
||||
private val MONTH_WORDS = mapOf(
|
||||
"januar" to 1, "január" to 1, "januara" to 1, "januára" to 1,
|
||||
"februar" to 2, "február" to 2, "februara" to 2, "februára" to 2,
|
||||
"marec" to 3, "marca" to 3,
|
||||
"april" to 4, "apríl" to 4, "aprila" to 4, "apríla" to 4,
|
||||
"maj" to 5, "máj" to 5, "maja" to 5, "mája" to 5,
|
||||
"jun" to 6, "jún" to 6, "juna" to 6, "júna" to 6,
|
||||
"jul" to 7, "júl" to 7, "jula" to 7, "júla" to 7,
|
||||
"august" to 8, "augusta" to 8,
|
||||
"september" to 9, "septembra" to 9,
|
||||
"oktober" to 10, "október" to 10, "oktobra" to 10, "októbra" to 10,
|
||||
"november" to 11, "novembra" to 11,
|
||||
"december" to 12, "decembra" to 12
|
||||
)
|
||||
|
||||
private fun monthFromWord(raw: String): Int? {
|
||||
val cleaned = raw
|
||||
.trim('.', ',', ';')
|
||||
.lowercase()
|
||||
return MONTH_WORDS[cleaned]
|
||||
}
|
||||
|
||||
private fun findAllNumbers(text: String): List<Int> {
|
||||
val regex = Regex("""\b(\d{1,2})\b""")
|
||||
return regex.findAll(text)
|
||||
.mapNotNull { it.groupValues[1].toIntOrNull() }
|
||||
.toList()
|
||||
}
|
||||
|
||||
fun parseDay(text: String): Int? {
|
||||
val nums = findAllNumbers(text)
|
||||
val digitDay = nums.firstOrNull { it in 1..31 }
|
||||
if (digitDay != null) return digitDay
|
||||
|
||||
// nie su cisla
|
||||
val wordRegex = Regex("""\b([\p{L}]+)\b""")
|
||||
for (m in wordRegex.findAll(text.lowercase())) {
|
||||
val word = m.groupValues[1]
|
||||
val num = numberFromWord(word) ?: continue
|
||||
if (num in 1..31) {
|
||||
return num
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun parseMonth(text: String): Int? {
|
||||
val normalized = text.lowercase()
|
||||
|
||||
// standartne nazvy
|
||||
val wordRegex = Regex("""\b([\p{L}]+)\b""")
|
||||
for (m in wordRegex.findAll(normalized)) {
|
||||
val word = m.groupValues[1]
|
||||
val month = monthFromWord(word)
|
||||
if (month != null) return month
|
||||
}
|
||||
|
||||
// cisla
|
||||
val ordinalMonthRegex = Regex("""\b([\p{L}]+)\s+mesiac\w*\b""")
|
||||
val ordMatch = ordinalMonthRegex.find(normalized)
|
||||
if (ordMatch != null) {
|
||||
val ordinalWord = ordMatch.groupValues[1]
|
||||
val num = numberFromWord(ordinalWord)
|
||||
if (num != null && num in 1..12) return num
|
||||
}
|
||||
|
||||
val nums = findAllNumbers(text)
|
||||
if (nums.size >= 2) {
|
||||
val month = nums[1]
|
||||
if (month in 1..12) return month
|
||||
}
|
||||
val single = nums.firstOrNull { it in 1..12 }
|
||||
return single
|
||||
}
|
||||
|
||||
// Dnesny datum --------------------
|
||||
fun isCorrectTodayDate(text: String): Boolean {
|
||||
val today = LocalDate.now()
|
||||
val day = parseDay(text) ?: return false
|
||||
val month = parseMonth(text)
|
||||
|
||||
return if (month == null) {
|
||||
// пользователь сказал только число
|
||||
day == today.dayOfMonth
|
||||
} else {
|
||||
// пользователь сказал и число, и месяц
|
||||
day == today.dayOfMonth && month == today.monthValue
|
||||
}
|
||||
}
|
||||
|
||||
// Zajtra ---------------------------------------------------
|
||||
fun isCorrectTomorrowDate(text: String): Boolean {
|
||||
val tomorrow = LocalDate.now().plusDays(1)
|
||||
val day = parseDay(text) ?: return false
|
||||
val month = parseMonth(text)
|
||||
|
||||
return if (month == null) {
|
||||
day == tomorrow.dayOfMonth
|
||||
} else {
|
||||
day == tomorrow.dayOfMonth && month == tomorrow.monthValue
|
||||
}
|
||||
}
|
||||
|
||||
// Mesiac -----------------------------------------
|
||||
fun isCorrectCurrentMonth(text: String): Boolean {
|
||||
val today = LocalDate.now()
|
||||
val month = parseMonth(text) ?: return false
|
||||
return month == today.monthValue
|
||||
}
|
||||
|
||||
// ---------- DEN TYZDN'A ----------
|
||||
|
||||
private val WEEKDAY_WORDS = mapOf(
|
||||
"pondelok" to DayOfWeek.MONDAY, "pondelka" to DayOfWeek.MONDAY,
|
||||
"utorok" to DayOfWeek.TUESDAY, "utorka" to DayOfWeek.TUESDAY,
|
||||
"streda" to DayOfWeek.WEDNESDAY, "stredu" to DayOfWeek.WEDNESDAY,
|
||||
"štvrtok" to DayOfWeek.THURSDAY, "stvrtok" to DayOfWeek.THURSDAY,
|
||||
"štvrtka" to DayOfWeek.THURSDAY, "stvrtka" to DayOfWeek.THURSDAY,
|
||||
"piatok" to DayOfWeek.FRIDAY, "piatka" to DayOfWeek.FRIDAY,
|
||||
"sobota" to DayOfWeek.SATURDAY, "sobotu" to DayOfWeek.SATURDAY,
|
||||
"nedeľa" to DayOfWeek.SUNDAY, "nedela" to DayOfWeek.SUNDAY, "nedeľu" to DayOfWeek.SUNDAY, "nedelu" to DayOfWeek.SUNDAY
|
||||
)
|
||||
|
||||
private fun weekdayFromWord(raw: String): DayOfWeek? {
|
||||
val cleaned = raw
|
||||
.trim('.', ',', ';')
|
||||
.lowercase()
|
||||
return WEEKDAY_WORDS[cleaned]
|
||||
}
|
||||
|
||||
fun parseWeekday(text: String): DayOfWeek? {
|
||||
val normalized = text.lowercase()
|
||||
val wordRegex = Regex("""\b([\p{L}]+)\b""")
|
||||
for (m in wordRegex.findAll(normalized)) {
|
||||
val word = m.groupValues[1]
|
||||
val weekday = weekdayFromWord(word)
|
||||
if (weekday != null) return weekday
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun isCorrectTodayWeekday(text: String): Boolean {
|
||||
val today = LocalDate.now()
|
||||
val weekday = parseWeekday(text) ?: return false
|
||||
|
||||
return weekday == today.dayOfWeek
|
||||
}
|
||||
|
||||
// ---------- DOBA ----------
|
||||
fun currentDayPeriod(): DayPeriod {
|
||||
val t = now().toLocalTime()
|
||||
return when {
|
||||
t.hour in 5..11 -> DayPeriod.MORNING
|
||||
t.hour in 12..17 -> DayPeriod.DAY
|
||||
else -> DayPeriod.EVENING
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDayPeriod(text: String): DayPeriod? {
|
||||
val normalized = text.lowercase()
|
||||
// ráno
|
||||
if (Regex("""\b(ráno|rano|doobeda|dopoludnie)\b""").containsMatchIn(normalized))
|
||||
return DayPeriod.MORNING
|
||||
// deň
|
||||
if (Regex("""\b(deň|den|obed|na obed|poobede|popoludnie)\b""").containsMatchIn(normalized))
|
||||
return DayPeriod.DAY
|
||||
// večer
|
||||
if (Regex("""\b(večer|vecer|podvečer|podvecer)\b""").containsMatchIn(normalized))
|
||||
return DayPeriod.EVENING
|
||||
return null
|
||||
}
|
||||
|
||||
/** Проверка: правильно ли пользователь назвал текущую "dennú dobu" (ráno / deň / večer). */
|
||||
fun isCorrectCurrentDayPeriod(text: String): Boolean {
|
||||
val userPeriod = parseDayPeriod(text) ?: return false
|
||||
val nowPeriod = currentDayPeriod()
|
||||
return userPeriod == nowPeriod
|
||||
}
|
||||
|
||||
// ---------- format pre robota ----------
|
||||
|
||||
private val MONTHS_NOMINATIVE = listOf(
|
||||
"január", "február", "marec", "apríl", "máj", "jún",
|
||||
"júl", "august", "september", "október", "november", "december"
|
||||
)
|
||||
|
||||
// private val MONTHS_GENITIVE = listOf(
|
||||
// "januára", "februára", "marca", "apríla", "mája", "júna",
|
||||
// "júla", "augusta", "septembra", "októbra", "novembra", "decembra"
|
||||
// )
|
||||
|
||||
private val WEEKDAYS = mapOf(
|
||||
DayOfWeek.MONDAY to "pondelok",
|
||||
DayOfWeek.TUESDAY to "utorok",
|
||||
DayOfWeek.WEDNESDAY to "streda",
|
||||
DayOfWeek.THURSDAY to "štvrtok",
|
||||
DayOfWeek.FRIDAY to "piatok",
|
||||
DayOfWeek.SATURDAY to "sobota",
|
||||
DayOfWeek.SUNDAY to "nedeľa"
|
||||
)
|
||||
|
||||
// hodiny
|
||||
fun CorrectCurrentTime(): String {
|
||||
val t = now().toLocalTime()
|
||||
return t.format(DateTimeFormatter.ofPattern("HH:mm"))
|
||||
}
|
||||
|
||||
// dnes
|
||||
fun CorrectTodayDate(): String = formatDate(LocalDate.now())
|
||||
|
||||
// zajtra
|
||||
fun CorrectTomorrowDate(): String = formatDate(LocalDate.now().plusDays(1))
|
||||
|
||||
// mesiac
|
||||
fun CorrectCurrentMonth(): String {
|
||||
val m = LocalDate.now().monthValue
|
||||
return MONTHS_NOMINATIVE[m - 1]
|
||||
}
|
||||
|
||||
// dan tyzdna
|
||||
fun CorrectTodayWeekday(): String {
|
||||
val d = LocalDate.now()
|
||||
val wd = WEEKDAYS[d.dayOfWeek] ?: d.dayOfWeek.name.lowercase()
|
||||
return "$wd"
|
||||
}
|
||||
|
||||
// doba
|
||||
fun CorrectCurrentDayPeriod(): String = when (currentDayPeriod()) {
|
||||
DayPeriod.MORNING -> "ráno"
|
||||
DayPeriod.DAY -> "deň"
|
||||
DayPeriod.EVENING -> "večer"
|
||||
}
|
||||
|
||||
private fun formatDate(d: LocalDate): String {
|
||||
val monthGen = MONTHS_NOMINATIVE[d.monthValue - 1]
|
||||
return "${d.dayOfMonth}. $monthGen"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,363 @@
|
||||
package furhatos.app.blank.flow.main.say_time
|
||||
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.flow.main.supporting.HintOffer
|
||||
import furhatos.app.blank.flow.main.supporting.calm
|
||||
import furhatos.app.blank.flow.main.supporting.general.callProxyRespond
|
||||
import furhatos.app.blank.flow.main.supporting.general.isProxyAvailable
|
||||
import furhatos.flow.kotlin.State
|
||||
import furhatos.flow.kotlin.furhat
|
||||
import furhatos.flow.kotlin.state
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.DayOfWeek
|
||||
import java.time.YearMonth
|
||||
|
||||
//-------- Hours ------------
|
||||
private fun hourQuarter(minute: Int): String = when (minute) {
|
||||
in 0..14 -> "prvej"
|
||||
in 15..29 -> "druhej"
|
||||
in 30..44 -> "tretej"
|
||||
else -> "štvrtej"
|
||||
}
|
||||
//------- Date ---------
|
||||
|
||||
private data class DateData(
|
||||
val monthName: String,
|
||||
val lastDay: Int, // 28/29/30/31
|
||||
val rangeStart: Int,
|
||||
val rangeEnd: Int,
|
||||
val section: String // opis pra napovedu
|
||||
)
|
||||
|
||||
private fun getDateData(date: LocalDate): DateData {
|
||||
val lastDay = YearMonth.of(date.year, date.month).lengthOfMonth()
|
||||
val day = date.dayOfMonth
|
||||
|
||||
val (start, end, label) = when {
|
||||
day <= 9 -> Triple(1, minOf(9, lastDay), "začiatok mesiaca")
|
||||
day <= 19 -> Triple(10, minOf(19, lastDay), "desiate dni mesiaca")
|
||||
day <= 26 -> Triple(20, minOf(26, lastDay), "dvadsiate dni mesiaca")
|
||||
else -> Triple(27, lastDay, "koniec mesiaca")
|
||||
}
|
||||
|
||||
return DateData(
|
||||
monthName = MonthNominative(date.monthValue),
|
||||
lastDay = lastDay,
|
||||
rangeStart = start,
|
||||
rangeEnd = end,
|
||||
section = label
|
||||
)
|
||||
}
|
||||
|
||||
//----------------------
|
||||
|
||||
private fun MonthNominative(m: Int): String = when (m) {
|
||||
1 -> "január"
|
||||
2 -> "február"
|
||||
3 -> "marec"
|
||||
4 -> "apríl"
|
||||
5 -> "máj"
|
||||
6 -> "jún"
|
||||
7 -> "júl"
|
||||
8 -> "august"
|
||||
9 -> "september"
|
||||
10 -> "október"
|
||||
11 -> "november"
|
||||
else -> "december"
|
||||
}
|
||||
|
||||
//--------- den tyzdna ----------
|
||||
private fun weekdays(d: java.time.DayOfWeek): String = when (d) {
|
||||
java.time.DayOfWeek.MONDAY -> "pondelok"
|
||||
java.time.DayOfWeek.TUESDAY -> "utorok"
|
||||
java.time.DayOfWeek.WEDNESDAY -> "streda"
|
||||
java.time.DayOfWeek.THURSDAY -> "štvrtok"
|
||||
java.time.DayOfWeek.FRIDAY -> "piatok"
|
||||
java.time.DayOfWeek.SATURDAY -> "sobota"
|
||||
java.time.DayOfWeek.SUNDAY -> "nedeľa"
|
||||
}
|
||||
|
||||
private fun weekdayIndex(d: java.time.DayOfWeek): Int = when (d) {
|
||||
java.time.DayOfWeek.MONDAY -> 1
|
||||
java.time.DayOfWeek.TUESDAY -> 2
|
||||
java.time.DayOfWeek.WEDNESDAY -> 3
|
||||
java.time.DayOfWeek.THURSDAY -> 4
|
||||
java.time.DayOfWeek.FRIDAY -> 5
|
||||
java.time.DayOfWeek.SATURDAY -> 6
|
||||
java.time.DayOfWeek.SUNDAY -> 7
|
||||
}
|
||||
private fun weekdayNumber(n: Int): String = when (n) {
|
||||
1 -> "prvý"
|
||||
2 -> "druhý"
|
||||
3 -> "tretí"
|
||||
4 -> "štvrtý"
|
||||
5 -> "piaty"
|
||||
6 -> "šiesty"
|
||||
else -> "siedmy"
|
||||
}
|
||||
|
||||
private fun prevDay(d: DayOfWeek): DayOfWeek =
|
||||
if (d == DayOfWeek.MONDAY) DayOfWeek.SUNDAY else DayOfWeek.of(d.value - 1)
|
||||
|
||||
private fun nextDay(d: DayOfWeek): DayOfWeek =
|
||||
if (d == DayOfWeek.SUNDAY) DayOfWeek.MONDAY else DayOfWeek.of(d.value + 1)
|
||||
|
||||
private fun weekSection(idx: Int): String = when (idx) {
|
||||
1, 2 -> "začiatok týždňa"
|
||||
3, 4 -> "stred týždňa"
|
||||
else -> "koniec týždňa"
|
||||
}
|
||||
|
||||
//----------- Mesiac ------------
|
||||
private data class SeasonMonth(
|
||||
val season: String,
|
||||
val n: Int
|
||||
)
|
||||
|
||||
private fun seasonMonth(month: Int): SeasonMonth = when (month) {
|
||||
12 -> SeasonMonth("zimy", 1)
|
||||
1 -> SeasonMonth("zimy", 2)
|
||||
2 -> SeasonMonth("zimy", 3)
|
||||
|
||||
3 -> SeasonMonth("jari", 1)
|
||||
4 -> SeasonMonth("jari", 2)
|
||||
5 -> SeasonMonth("jari", 3)
|
||||
|
||||
6 -> SeasonMonth("leta", 1)
|
||||
7 -> SeasonMonth("leta", 2)
|
||||
8 -> SeasonMonth("leta", 3)
|
||||
|
||||
9 -> SeasonMonth("jesene", 1)
|
||||
10 -> SeasonMonth("jesene", 2)
|
||||
else -> SeasonMonth("jesene", 3)
|
||||
}
|
||||
|
||||
private fun seasonNumber(n: Int): String = when (n) {
|
||||
1 -> "prvý"
|
||||
2 -> "druhý"
|
||||
else -> "tretí"
|
||||
}
|
||||
|
||||
private fun season(month: Int): String = when (month) {
|
||||
12, 1, 2 -> "Teraz je zima."
|
||||
3, 4, 5 -> "Teraz je jar."
|
||||
6, 7, 8 -> "Teraz je leto."
|
||||
else -> "Teraz je jeseň."
|
||||
}
|
||||
|
||||
private fun buildHintContext(type: TimeQuestionType, baseHint: String): Map<String, Any?> {
|
||||
val ctx = mutableMapOf<String, Any?>(
|
||||
"task" to "hint",
|
||||
"language" to "sk",
|
||||
"question_type" to type.name,
|
||||
"base_hint" to baseHint,
|
||||
"rules" to mapOf(
|
||||
"do_not_reveal_exact_answer" to true,
|
||||
"max_sentences" to 2,
|
||||
"keep_game_rules" to true
|
||||
)
|
||||
)
|
||||
|
||||
// Если у тебя есть lastQuestionText (мы добавляли для explain_wrong) — раскомментируй:
|
||||
// ctx["question"] = lastQuestionText
|
||||
|
||||
when (type) {
|
||||
TimeQuestionType.CURRENT_TIME -> {
|
||||
val now = LocalTime.now()
|
||||
val h = now.hour
|
||||
val next = (h + 1) % 24
|
||||
ctx["hour_from"] = if (h == 0) "0" else h.toString()
|
||||
ctx["hour_to"] = if (next == 0) "0" else next.toString()
|
||||
ctx["minute_hint"] = hourQuarter(now.minute)
|
||||
ctx["suggested_formats"] = listOf("je päť hodín", "5:00", "okolo piatej")
|
||||
}
|
||||
|
||||
TimeQuestionType.TODAY_DATE -> {
|
||||
val d = getDateData(LocalDate.now())
|
||||
ctx["expected_format"] = "DD.MM.YYYY"
|
||||
ctx["range_start"] = d.rangeStart
|
||||
ctx["range_end"] = d.rangeEnd
|
||||
ctx["section"] = d.section
|
||||
ctx["month_name"] = d.monthName
|
||||
ctx["year"] = LocalDate.now().year
|
||||
}
|
||||
|
||||
TimeQuestionType.TOMORROW_DATE -> {
|
||||
val today = LocalDate.now()
|
||||
val tomorrow = today.plusDays(1)
|
||||
val d = getDateData(tomorrow)
|
||||
val isNewMonth = tomorrow.dayOfMonth == 1 || tomorrow.month != today.month
|
||||
|
||||
ctx["expected_format"] = "DD.MM.YYYY"
|
||||
ctx["rule"] = "Zajtra je o jeden deň neskôr ako dnes."
|
||||
ctx["is_new_month"] = isNewMonth
|
||||
ctx["month_name"] = d.monthName
|
||||
ctx["range_start"] = d.rangeStart
|
||||
ctx["range_end"] = d.rangeEnd
|
||||
ctx["section"] = d.section
|
||||
ctx["year"] = tomorrow.year
|
||||
}
|
||||
|
||||
TimeQuestionType.CURRENT_MONTH -> {
|
||||
val month = LocalDate.now().monthValue
|
||||
val info = seasonMonth(month)
|
||||
ctx["answer_kind"] = "month_name"
|
||||
ctx["season"] = info.season
|
||||
ctx["month_in_season"] = info.n
|
||||
ctx["allowed_hints"] = listOf("season", "month_number_without_name", "first_letter")
|
||||
}
|
||||
|
||||
TimeQuestionType.TODAY_WEEKDAY -> {
|
||||
val today = LocalDate.now()
|
||||
val wd = today.dayOfWeek
|
||||
val i = weekdayIndex(wd)
|
||||
|
||||
ctx["answer_kind"] = "weekday_name"
|
||||
ctx["weekday_index"] = i
|
||||
ctx["week_section"] = weekSection(i)
|
||||
ctx["yesterday"] = weekdays(prevDay(wd))
|
||||
ctx["tomorrow"] = weekdays(nextDay(wd))
|
||||
ctx["allowed_hints"] = listOf("weekday_or_weekend", "order_in_week_without_name")
|
||||
}
|
||||
|
||||
TimeQuestionType.DAY_PERIOD -> {
|
||||
ctx["options"] = listOf("ráno", "deň", "večer")
|
||||
ctx["rough_ranges"] = mapOf(
|
||||
"ráno" to "5–11",
|
||||
"deň" to "12–17",
|
||||
"večer" to "18–23"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------
|
||||
|
||||
val TimeHintOffer: State by lazy {
|
||||
HintOffer(nextState = TimeHint, exitState = AskTime)
|
||||
}
|
||||
|
||||
val TimeHint = state(Parent) {
|
||||
onEntry {
|
||||
hintUsedForCurrentQuestion = true
|
||||
val baseHint = when (currentTimeQuestionType) {
|
||||
|
||||
TimeQuestionType.CURRENT_TIME -> {
|
||||
val now = LocalTime.now()
|
||||
val h = now.hour
|
||||
val next = (h + 1) % 24
|
||||
val quarter = hourQuarter(now.minute)
|
||||
|
||||
val hourFrom = if (h == 0) "0" else h.toString()
|
||||
val hourTo = if (next == 0) "0" else next.toString()
|
||||
|
||||
val hints = listOf(
|
||||
"Malá nápoveda: je to medzi $hourFrom a $hourTo hodinou.",
|
||||
"Dobre! Tu je moja nápoveda: teraz je čas niekde medzi $hourFrom a $hourTo hodinou, skôr v $quarter časti tejto hodiny.",
|
||||
"Skúste odhad: je to medzi $hourFrom a $hourTo hodinou. A minúty sú približne v $quarter časti hodiny."
|
||||
)
|
||||
|
||||
calm()
|
||||
hints.random()
|
||||
}
|
||||
|
||||
TimeQuestionType.TODAY_DATE -> {
|
||||
val d = getDateData(LocalDate.now())
|
||||
|
||||
val hints = listOf(
|
||||
"Pomôcka: dnešné číslo je medzi ${d.rangeStart} a ${d.rangeEnd}",
|
||||
"Teraz je ${d.section}, teda číslo je medzi ${d.rangeStart} a ${d.rangeEnd}."
|
||||
)
|
||||
|
||||
calm()
|
||||
hints.random()
|
||||
}
|
||||
|
||||
TimeQuestionType.TOMORROW_DATE -> {
|
||||
val today = LocalDate.now()
|
||||
val tomorrow = today.plusDays(1)
|
||||
val d = getDateData(tomorrow)
|
||||
|
||||
val isNewMonth = tomorrow.dayOfMonth == 1 || tomorrow.month != today.month
|
||||
|
||||
val hints = mutableListOf<String>()
|
||||
|
||||
if (!isNewMonth) {
|
||||
hints += "Malá nápoveda: zajtra bude stále v mesiaci ${d.monthName}."
|
||||
}
|
||||
if (isNewMonth) {
|
||||
hints += "Malá nápoveda: zajtra už bude v mesiaci ${d.monthName}."
|
||||
}
|
||||
|
||||
hints += "Zajtra spadá do časti ${d.section}. Skúste číslo medzi ${d.rangeStart} a ${d.rangeEnd}."
|
||||
hints += "Dobre! Tu je moja nápoveda: číslo zajtrajšieho dňa je medzi ${d.rangeStart} a ${d.rangeEnd}"
|
||||
|
||||
calm()
|
||||
hints.random()
|
||||
}
|
||||
|
||||
TimeQuestionType.CURRENT_MONTH -> {
|
||||
|
||||
|
||||
val month = LocalDate.now().monthValue
|
||||
val info = seasonMonth(month)
|
||||
val number = seasonNumber(info.n)
|
||||
|
||||
val hints = listOf(
|
||||
"Malá nápoveda: teraz je $number mesiac ${info.season}. Pamätáte si, ktorý mesiac to je?",
|
||||
"Dobre! Tu je moja nápoveda: sme v $number mesiaci ${info.season}.",
|
||||
"Fajn! Skúsme to takto: ${season(month)}, čiže ide o $number mesiac ${info.season}. Ktorý mesiac to môže byť?",
|
||||
)
|
||||
|
||||
calm()
|
||||
hints.random()
|
||||
}
|
||||
|
||||
TimeQuestionType.TODAY_WEEKDAY -> {
|
||||
val today = LocalDate.now()
|
||||
val wd = today.dayOfWeek
|
||||
|
||||
val i = weekdayIndex(today.dayOfWeek)
|
||||
val n = weekdayNumber(i)
|
||||
|
||||
val yesterdayWd = weekdays(prevDay(wd))
|
||||
val tomorrowWd = weekdays(nextDay(wd))
|
||||
|
||||
val section = weekSection(i)
|
||||
|
||||
val hints = listOf(
|
||||
"Fajn! Moja malá nápoveda: dnes je $n deň v týždni.",
|
||||
"Tak teda, moja nápoveda: v týždni je to $n deň (počítame od pondelka).",
|
||||
"Včera bol $yesterdayWd a zajtra bude $tomorrowWd. Aký deň je potom dnes?",
|
||||
"Teraz je $section. Skúste si spomenúť, aký to je konkrétny deň."
|
||||
)
|
||||
|
||||
calm()
|
||||
hints.random()
|
||||
}
|
||||
|
||||
TimeQuestionType.DAY_PERIOD -> {
|
||||
calm()
|
||||
"Fajn! Malá pomôcka: ráno je približne päť až jedenásť, deň dvanásť až sedemnásť a večer osemnásť až dvadsaťtri. Čo z toho je teraz najbližšie?"
|
||||
}
|
||||
}
|
||||
|
||||
val ctx = buildHintContext(currentTimeQuestionType, baseHint)
|
||||
|
||||
val gptHint = if (isProxyAvailable()) {
|
||||
callProxyRespond(
|
||||
userText = "HINT_REQUEST",
|
||||
task = "hint",
|
||||
context = ctx
|
||||
)
|
||||
} else null
|
||||
|
||||
val finalHint = if (!gptHint.isNullOrBlank()) gptHint else baseHint
|
||||
furhat.say(finalHint)
|
||||
|
||||
goto(AskTime)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package furhatos.app.blank.flow.main.supporting
|
||||
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.nlu.base_answer.Ano
|
||||
import furhatos.app.blank.nlu.base_answer.Nie
|
||||
import furhatos.flow.kotlin.*
|
||||
|
||||
fun AskToContinue() = state(Parent) {
|
||||
|
||||
onEntry {
|
||||
furhat.ask(
|
||||
"Chcete pokračovať hrať alebo by ste chceli skončiť?"
|
||||
)
|
||||
}
|
||||
|
||||
onResponse<Ano> {
|
||||
veryHappy()
|
||||
furhat.say("Výborne, budeme pokračovať.")
|
||||
delay(700)
|
||||
terminate(true)
|
||||
}
|
||||
|
||||
onResponse<Nie> {
|
||||
empathy()
|
||||
furhat.say("Rozumiem, nebudeme pokračovať hráť v tuto hru.")
|
||||
delay(700)
|
||||
terminate(false)
|
||||
}
|
||||
|
||||
onResponse {
|
||||
furhat.say("Prepáčte, nerozumel som. Odpovedzte prosím chcete pokračovať alebo nechcete.")
|
||||
delay(700)
|
||||
reentry()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,106 @@
|
||||
package furhatos.app.blank.flow.main.supporting
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.flow.main.StartQuestion
|
||||
import furhatos.app.blank.flow.main.handlers.Goodbye
|
||||
import furhatos.app.blank.flow.main.handlers.handleRepeat
|
||||
import furhatos.app.blank.flow.main.handlers.handleRephrase
|
||||
import furhatos.app.blank.flow.main.handlers.handleStop
|
||||
import furhatos.app.blank.flow.main.supporting.general.TrainingMenuFlags
|
||||
import furhatos.flow.kotlin.*
|
||||
import furhatos.app.blank.nlu.other_responses.FeelGood
|
||||
import furhatos.app.blank.nlu.other_responses.FeelBad
|
||||
import furhatos.app.blank.nlu.base_answer.Ano
|
||||
import furhatos.app.blank.nlu.base_answer.Nie
|
||||
|
||||
|
||||
val CheckCondition: State = state(Parent) {
|
||||
|
||||
onEntry {
|
||||
TrainingMenuFlags.resetAll()
|
||||
|
||||
furhat.ask {
|
||||
random {
|
||||
+"Ako sa máte?"
|
||||
+"Ako sa dnes cítite?"
|
||||
+"Ako sa vám dnes darí?"
|
||||
+"Máte sa dobre?"
|
||||
+"Ako sa cítite práve teraz?"
|
||||
+"Ako sa cítite po dnešku?"
|
||||
+"Je vám dnes dobre?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onResponse<FeelGood> {
|
||||
veryHappy()
|
||||
furhat.say("Som rád, že sa cítite dobre.")
|
||||
delay(1000)
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
}
|
||||
|
||||
|
||||
onResponse<FeelBad> {
|
||||
empathy()
|
||||
furhat.say(
|
||||
"Rozumiem, nechcem vás dnes zaťažovať. "
|
||||
+ "Dnes je lepšie odpočívať."
|
||||
)
|
||||
goto(Goodbye)
|
||||
}
|
||||
|
||||
onResponse {
|
||||
happyNod()
|
||||
furhat.say("Ďakujem za odpoveď. Môžme skusíť si napriek tomu krátku hru.")
|
||||
goto(StartQuestion)
|
||||
}
|
||||
}
|
||||
|
||||
fun ReadyToTrain(nextState: State, exitState: State): State = state(Parent) {
|
||||
|
||||
fun prompt(): String {
|
||||
return if (TrainingMenuFlags.allExercisesCompleted())
|
||||
"Dnes sme už už odohrali všetky hry. Chcete si niektoré zopakovať?"
|
||||
else if (TrainingMenuFlags.hasTrainedOnce)
|
||||
"Chcete si zahrať inú hru?"
|
||||
else
|
||||
"Chcete si zahrať hru?"
|
||||
}
|
||||
|
||||
onEntry {
|
||||
furhat.ask(prompt())
|
||||
}
|
||||
|
||||
onReentry {
|
||||
furhat.ask(prompt())
|
||||
}
|
||||
|
||||
onResponse<Ano> {
|
||||
veryHappy()
|
||||
furhat.say("Výborne, tak poďme na to.")
|
||||
delay(1000)
|
||||
goto(nextState)
|
||||
}
|
||||
|
||||
onResponse<Nie> {
|
||||
empathy()
|
||||
furhat.say("Rozumiem, môžeme to skúsiť inokedy.")
|
||||
goto(exitState)
|
||||
}
|
||||
|
||||
onResponse {
|
||||
if (handleRephrase(it.intent)){
|
||||
return@onResponse
|
||||
}
|
||||
// if (handleRepeat(it.intent)){
|
||||
// return@onResponse
|
||||
// }
|
||||
if (handleStop(it.intent, "system", "check_condition", it.text ?: "")){
|
||||
goto(Goodbye)
|
||||
return@onResponse
|
||||
}
|
||||
|
||||
furhat.say("Prepáčte, nerozumela som.")
|
||||
reentry()
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
package furhatos.app.blank.flow.main.supporting
|
||||
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.nlu.base_answer.Ano
|
||||
import furhatos.app.blank.nlu.base_answer.Nie
|
||||
import furhatos.flow.kotlin.State
|
||||
import furhatos.flow.kotlin.furhat
|
||||
import furhatos.flow.kotlin.onResponse
|
||||
import furhatos.flow.kotlin.state
|
||||
|
||||
val AskIncreaseDifficulty: State = state(Parent) {
|
||||
onEntry {
|
||||
furhat.ask("Chcete, aby som povedal viac slov?")
|
||||
}
|
||||
|
||||
onResponse<Ano> { terminate(true) }
|
||||
onResponse<Nie> { terminate(false) }
|
||||
|
||||
onResponse {
|
||||
furhat.say("Prepáčte, odpovedzte prosím áno alebo nie.")
|
||||
reentry()
|
||||
}
|
||||
}
|
||||
|
||||
val AskDecreaseDifficulty: State = state(Parent) {
|
||||
onEntry {
|
||||
furhat.ask("Chcete sa vrátiť na predchádzajúcu, ľahšiu úroveň náročnosti?")
|
||||
}
|
||||
|
||||
onResponse<Ano> { terminate(true) }
|
||||
onResponse<Nie> { terminate(false) }
|
||||
|
||||
onResponse {
|
||||
furhat.say("Prepáčte, odpovedzte prosím áno alebo nie.")
|
||||
reentry()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package furhatos.app.blank.flow.main.supporting
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.flow.main.Idle
|
||||
import furhatos.app.blank.flow.main.handlers.handleRepeat
|
||||
import furhatos.app.blank.flow.main.handlers.handleStop
|
||||
import furhatos.flow.kotlin.*
|
||||
import furhatos.app.blank.nlu.base_answer.Ano
|
||||
import furhatos.app.blank.nlu.base_answer.Nie
|
||||
import furhatos.gestures.Gestures
|
||||
|
||||
|
||||
fun HintOffer(nextState: State, exitState: State): State = state(Parent) {
|
||||
onEntry {
|
||||
calm()
|
||||
furhat.ask {
|
||||
random {
|
||||
+"Chcete malú nápovedu?"
|
||||
+"Pomôže Vám malá nápoveda?"
|
||||
+"Možno by som mala Vám dať nápovedu?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onResponse<Ano> {
|
||||
calm()
|
||||
goto(nextState)
|
||||
}
|
||||
|
||||
onResponse<Nie> {
|
||||
empathy()
|
||||
furhat.say("Dobre! Tak potom jednoducho budeme pokračovať.")
|
||||
goto(exitState)
|
||||
}
|
||||
|
||||
onResponse {
|
||||
furhat.say {
|
||||
random {
|
||||
+"Stačí povedať áno alebo nie."
|
||||
+"Prosím, povedzte áno alebo nie."
|
||||
}
|
||||
}
|
||||
reentry()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package furhatos.app.blank.flow.main.supporting
|
||||
|
||||
import furhatos.flow.kotlin.FlowControlRunner
|
||||
import furhatos.flow.kotlin.furhat
|
||||
import furhatos.gestures.BasicParams
|
||||
import furhatos.gestures.Gesture
|
||||
import furhatos.gestures.Gestures
|
||||
import furhatos.gestures.defineGesture
|
||||
|
||||
fun FlowControlRunner.happyNod(async: Boolean = true) {
|
||||
furhat.gesture(Gestures.Smile, async = async)
|
||||
furhat.gesture(Gestures.Nod, async = async)
|
||||
}
|
||||
|
||||
fun FlowControlRunner.happyShake(async: Boolean = true) {
|
||||
furhat.gesture(Gestures.Shake, async = async)
|
||||
furhat.gesture(Gestures.Smile, async = async)
|
||||
}
|
||||
|
||||
val LittleSadHeadDown: Gesture = defineGesture("LittleSadHeadDown") {
|
||||
frame(0.25) {
|
||||
BasicParams.NECK_TILT to 2.0
|
||||
}
|
||||
frame(0.55) {
|
||||
BasicParams.NECK_TILT to 5.0
|
||||
}
|
||||
frame(0.9, 2.0) {
|
||||
BasicParams.NECK_TILT to 9.0
|
||||
}
|
||||
|
||||
// плавное возвращение
|
||||
frame(2.35) {
|
||||
BasicParams.NECK_TILT to 5.0
|
||||
}
|
||||
frame(2.65) {
|
||||
BasicParams.NECK_TILT to 1.0
|
||||
}
|
||||
reset(3.0)
|
||||
}
|
||||
fun FlowControlRunner.littleSad(async: Boolean = true) {
|
||||
// грустная эмоция
|
||||
furhat.gesture(Gestures.ExpressSad(strength = 0.4, duration = 0.8), async = true)
|
||||
|
||||
// лёгкий наклон головы вниз примерно на 2 секунды
|
||||
furhat.gesture(LittleSadHeadDown, async = async)
|
||||
}
|
||||
|
||||
fun FlowControlRunner.empathy(async: Boolean = true) {
|
||||
furhat.gesture(Gestures.CloseEyes, async = false)
|
||||
furhat.gesture(Gestures.Nod, async = async)
|
||||
delay(700)
|
||||
furhat.gesture(Gestures.OpenEyes, async = false)
|
||||
}
|
||||
|
||||
fun FlowControlRunner.calm(async: Boolean = true) {
|
||||
furhat.gesture(Gestures.Smile, async = async)
|
||||
furhat.gesture(Gestures.Blink, async = async)
|
||||
}
|
||||
|
||||
fun FlowControlRunner.veryHappy(async: Boolean = true) {
|
||||
furhat.gesture(Gestures.BigSmile, async = async)
|
||||
furhat.gesture(Gestures.BrowRaise, async = async)
|
||||
}
|
||||
|
||||
val SmileIdle: Gesture = defineGesture("SmileIdle") {
|
||||
frame(0.2, persist = true) {
|
||||
BasicParams.SMILE_CLOSED to 0.70
|
||||
BasicParams.BROW_UP_LEFT to 0.05
|
||||
BasicParams.BROW_UP_RIGHT to 0.05
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
package furhatos.app.blank.flow.main.supporting
|
||||
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.flow.main.StartQuestion
|
||||
import furhatos.app.blank.flow.main.handlers.Goodbye
|
||||
import furhatos.app.blank.flow.main.handlers.askRepeatable
|
||||
import furhatos.app.blank.flow.main.handlers.handleRepeat
|
||||
import furhatos.app.blank.flow.main.handlers.handleRephrase
|
||||
import furhatos.app.blank.flow.main.handlers.handleStop
|
||||
import furhatos.app.blank.flow.main.say_time.TimeTrainingSuccess
|
||||
import furhatos.app.blank.flow.main.supporting.general.TrainingMenuFlags
|
||||
import furhatos.app.blank.nlu.base_answer.Ano
|
||||
import furhatos.app.blank.nlu.base_answer.Rephrase
|
||||
import furhatos.flow.kotlin.*
|
||||
|
||||
fun neutralAck(): String = listOf(
|
||||
"Rozumiem, ďakujem.",
|
||||
"Rozumiem. Vážim si, že ste mi to povedali.",
|
||||
"Ďakujem, že ste mi to povedali.",
|
||||
"Je dobre, že o tom môžeme hovoriť.",
|
||||
"Ďakujem, že ste mi venovali čas a porozprávali mi o tom.",
|
||||
"Dobre, vnímam to. Ďakujem, že ste sa otvorili.",
|
||||
"Ďakujem za vaše slová.",
|
||||
).random()
|
||||
|
||||
fun positiveAck(): String = listOf(
|
||||
"To ma veľmi teší.",
|
||||
"To je príjemné počuť.",
|
||||
"Som rada, že to takto vnímate.",
|
||||
"To znie naozaj dobre.",
|
||||
"Teší ma, že sa vám darí.",
|
||||
"To je skvelé, ďakujem, že ste sa podelili.",
|
||||
).random()
|
||||
|
||||
//private fun negativeAck(): String = listOf(
|
||||
// "To ma mrzí.",
|
||||
// "Je mi ľúto, že to takto dopadlo.",
|
||||
// "Chápem. Musí to byť náročné.",
|
||||
// "Mrzí ma to počuť. Verím, že sa situácia postupne zlepší.",
|
||||
//).random()
|
||||
|
||||
fun SmallTalk(nextState: State, question: () -> String): State = state(Parent) {
|
||||
onEntry {
|
||||
askRepeatable(question())
|
||||
}
|
||||
|
||||
onResponse<Ano> {
|
||||
happyNod()
|
||||
furhat.say(positiveAck())
|
||||
delay(1000)
|
||||
goto(nextState)
|
||||
}
|
||||
|
||||
onResponse<Rephrase> {
|
||||
handleRephrase(it.intent)
|
||||
}
|
||||
|
||||
onResponse {
|
||||
// if (handleRepeat(it.intent)) {
|
||||
// return@onResponse
|
||||
// }
|
||||
if (handleStop(it.intent, "system", "small_talk", it.text ?: "")) {
|
||||
TrainingMenuFlags.hasTrainedOnce = true
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
return@onResponse
|
||||
}
|
||||
|
||||
happyNod()
|
||||
furhat.say(neutralAck())
|
||||
delay(1000)
|
||||
goto(nextState)
|
||||
}
|
||||
}
|
||||
|
||||
fun SmallTalk(question: () -> String): State = SmallTalk(TimeTrainingSuccess, question)
|
||||
@ -0,0 +1,52 @@
|
||||
package furhatos.app.blank.flow.main.supporting
|
||||
|
||||
import furhatos.app.blank.flow.main.supporting.general.callProxyRespond
|
||||
import furhatos.app.blank.flow.main.supporting.general.isProxyAvailable
|
||||
import furhatos.flow.kotlin.FlowControlRunner
|
||||
import furhatos.flow.kotlin.furhat
|
||||
|
||||
fun FlowControlRunner.sayPhraseForWrongAnswer() {
|
||||
littleSad()
|
||||
furhat.say {
|
||||
random {
|
||||
+"Nie celkom, ale nevadí. "
|
||||
+"Tentoraz to ešte nesedí. "
|
||||
+"Ešte to nie je správne. "
|
||||
+"Nie úplne, ale sme blízko. "
|
||||
+"Ešte sa netrafili, ale je to v poriadku. "
|
||||
+"Zatiaľ to nesedí. "
|
||||
+"Nie je to správne, no vôbec to neprekáža. "
|
||||
+"Bohužiaľ, opäť je to nesprávne. Ale nič sa nedeje!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun FlowControlRunner.explainWhyWrong(
|
||||
question: String,
|
||||
correctAnswer: String,
|
||||
userAnswer: String,
|
||||
attempt: Int
|
||||
) {
|
||||
if (attempt >= 2) {
|
||||
sayPhraseForWrongAnswer()
|
||||
|
||||
val ctx = mapOf(
|
||||
"question" to question,
|
||||
"correct_answer" to correctAnswer,
|
||||
"user_answer" to userAnswer,
|
||||
"attempt" to attempt
|
||||
)
|
||||
|
||||
val gptText = if (isProxyAvailable()){
|
||||
callProxyRespond(
|
||||
userText = userAnswer,
|
||||
task = "explain_wrong",
|
||||
context = ctx
|
||||
)
|
||||
} else null
|
||||
|
||||
if (!gptText.isNullOrBlank()) {
|
||||
furhat.say(gptText)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,181 @@
|
||||
package furhatos.app.blank.flow.main.supporting.general
|
||||
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.time.Instant
|
||||
|
||||
// IP PC, where is running FastAPI-proxy.
|
||||
private const val PROXY_URL = "http://127.0.0.1:8000"
|
||||
|
||||
val PROXY_BASE_URL: String =
|
||||
System.getenv("PROXY_BASE_URL")
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: System.getProperty("proxy.baseUrl")
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: PROXY_URL
|
||||
|
||||
|
||||
fun isProxyAvailable(): Boolean {
|
||||
return try {
|
||||
val url = URL("$PROXY_BASE_URL/test")
|
||||
val conn = (url.openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
setRequestProperty("Accept", "application/json")
|
||||
connectTimeout = 500
|
||||
readTimeout = 500
|
||||
}
|
||||
|
||||
val code = conn.responseCode
|
||||
code in 200..299
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun callProxyRespond(
|
||||
userText: String,
|
||||
task: String,
|
||||
context: Map<String, Any?> = emptyMap()
|
||||
): String? {
|
||||
val url = URL("$PROXY_BASE_URL/respond")
|
||||
val conn = (url.openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "POST"
|
||||
setRequestProperty("Content-Type", "application/json; charset=UTF-8")
|
||||
setRequestProperty("Accept", "application/json")
|
||||
connectTimeout = 4000
|
||||
readTimeout = 4000
|
||||
doOutput = true
|
||||
}
|
||||
|
||||
val safeText = jsonEscape(userText)
|
||||
val safeTask = jsonEscape(task)
|
||||
val ctxJson = mapToJson(context)
|
||||
|
||||
// context вставляем как JSON-объект, а не строкой
|
||||
val body = """{"user_text":"$safeText","task":"$safeTask","context":$ctxJson}"""
|
||||
|
||||
return try {
|
||||
conn.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) }
|
||||
|
||||
val code = conn.responseCode
|
||||
val stream = if (code in 200..299) conn.inputStream else conn.errorStream
|
||||
val raw = stream?.bufferedReader(Charsets.UTF_8)?.use { it.readText() }
|
||||
|
||||
println("Proxy respond: $PROXY_BASE_URL/respond -> HTTP $code")
|
||||
println("Proxy raw response: $raw")
|
||||
|
||||
if (code !in 200..299 || raw.isNullOrBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Простой парсинг поля "answer"
|
||||
val match = Regex("\"answer\"\\s*:\\s*\"(.*?)\"").find(raw) ?: return null
|
||||
match.groupValues[1]
|
||||
.replace("\\n", "\n")
|
||||
.replace("\\\"", "\"")
|
||||
.replace("\\\\", "\\")
|
||||
} catch (e: Exception) {
|
||||
println("Proxy request failed: ${e.message}")
|
||||
null
|
||||
} finally {
|
||||
conn.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonEscape(s: String): String =
|
||||
s.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t")
|
||||
|
||||
private fun valueToJson(v: Any?): String = when (v) {
|
||||
null -> "null"
|
||||
is String -> "\"${jsonEscape(v)}\""
|
||||
is Number, is Boolean -> v.toString()
|
||||
is Map<*, *> -> mapToJson(v.entries.associate { it.key.toString() to it.value })
|
||||
is List<*> -> v.joinToString(prefix = "[", postfix = "]") { valueToJson(it) }
|
||||
else -> "\"${jsonEscape(v.toString())}\""
|
||||
}
|
||||
|
||||
private fun mapToJson(map: Map<String, Any?>): String =
|
||||
map.entries.joinToString(prefix = "{", postfix = "}") { (k, v) ->
|
||||
"\"${jsonEscape(k)}\":${valueToJson(v)}"
|
||||
}
|
||||
|
||||
fun sendLogEvent(
|
||||
createdAt: String,
|
||||
sessionId: String,
|
||||
game: String,
|
||||
phase: String,
|
||||
question: String,
|
||||
attempt: Int,
|
||||
result: String,
|
||||
userAnswer: String? = null,
|
||||
correctAnswer: String? = null,
|
||||
hintUsed: Boolean = false,
|
||||
): Boolean {
|
||||
val url = URL("$PROXY_BASE_URL/log")
|
||||
val conn = (url.openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "POST"
|
||||
setRequestProperty("Content-Type", "application/json")
|
||||
connectTimeout = 700
|
||||
readTimeout = 1000
|
||||
doOutput = true
|
||||
}
|
||||
|
||||
val payload = mapOf(
|
||||
"session_id" to sessionId,
|
||||
"game" to game,
|
||||
"phase" to phase,
|
||||
"question" to question,
|
||||
"attempt" to attempt,
|
||||
"result" to result,
|
||||
"user_answer" to userAnswer,
|
||||
"correct_answer" to correctAnswer,
|
||||
"hint_used" to hintUsed,
|
||||
"created_at" to createdAt
|
||||
)
|
||||
|
||||
val body = mapToJson(payload)
|
||||
|
||||
return try {
|
||||
conn.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) }
|
||||
|
||||
val code = conn.responseCode
|
||||
println("Proxy log: $PROXY_BASE_URL/log -> HTTP $code")
|
||||
|
||||
code in 200..299
|
||||
} catch (e: Exception) {
|
||||
println("LOG SEND ERROR: ${e.message}")
|
||||
false
|
||||
} finally {
|
||||
conn.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
fun sendLogJsonLine(jsonLine: String): Boolean {
|
||||
val url = URL("$PROXY_BASE_URL/log")
|
||||
val conn = (url.openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "POST"
|
||||
setRequestProperty("Content-Type", "application/json; charset=UTF-8")
|
||||
setRequestProperty("Accept", "application/json")
|
||||
connectTimeout = 600
|
||||
readTimeout = 900
|
||||
doOutput = true
|
||||
}
|
||||
|
||||
return try {
|
||||
conn.outputStream.use { it.write(jsonLine.toByteArray(Charsets.UTF_8)) }
|
||||
|
||||
val code = conn.responseCode
|
||||
println("sendLogJsonLine -> HTTP $code")
|
||||
|
||||
code in 200..299
|
||||
} catch (e: Exception) {
|
||||
println("sendLogJsonLine ERROR: ${e.message}")
|
||||
false
|
||||
} finally {
|
||||
conn.disconnect()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package furhatos.app.blank.flow.main.supporting.general
|
||||
|
||||
object TrainingMenuFlags {
|
||||
var hasTrainedOnce: Boolean = false
|
||||
|
||||
var allTimeQuestionsCompleted: Boolean = false
|
||||
var allAttentionQuestionsCompleted: Boolean = false
|
||||
var allMemoryQuestionsCompleted: Boolean = false
|
||||
|
||||
fun resetAll() {
|
||||
allTimeQuestionsCompleted = false
|
||||
allAttentionQuestionsCompleted = false
|
||||
allMemoryQuestionsCompleted = false
|
||||
}
|
||||
|
||||
fun allExercisesCompleted(): Boolean =
|
||||
allTimeQuestionsCompleted && allAttentionQuestionsCompleted && allMemoryQuestionsCompleted
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
package furhatos.app.blank.flow.main.supporting.general
|
||||
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.flow.main.StartQuestion
|
||||
import furhatos.app.blank.flow.main.handlers.Goodbye
|
||||
import furhatos.app.blank.flow.main.handlers.askRepeatable
|
||||
import furhatos.app.blank.flow.main.handlers.handleRepeat
|
||||
import furhatos.app.blank.flow.main.handlers.handleRephrase
|
||||
import furhatos.app.blank.flow.main.handlers.handleStop
|
||||
import furhatos.app.blank.flow.main.handlers.lastPhrase
|
||||
import furhatos.app.blank.flow.main.supporting.ReadyToTrain
|
||||
import furhatos.app.blank.flow.main.supporting.empathy
|
||||
import furhatos.app.blank.flow.main.supporting.neutralAck
|
||||
import furhatos.app.blank.flow.main.supporting.veryHappy
|
||||
import furhatos.flow.kotlin.State
|
||||
import furhatos.flow.kotlin.furhat
|
||||
import furhatos.flow.kotlin.onNoResponse
|
||||
import furhatos.flow.kotlin.onResponse
|
||||
import furhatos.flow.kotlin.state
|
||||
|
||||
data class SmallTalkContext(
|
||||
val exercise: String,
|
||||
val topic: String,
|
||||
val subtopic: String? = null,
|
||||
val targetWord: String? = null,
|
||||
val responseMode: String = "open" // open / yes_no
|
||||
)
|
||||
|
||||
fun requestSmallTalk(context: SmallTalkContext): String {
|
||||
if (!isProxyAvailable()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return callProxyRespond(
|
||||
userText = "Vygeneruj small talk otázku",
|
||||
task = "smalltalk",
|
||||
context = mapOf(
|
||||
"exercise" to context.exercise,
|
||||
"topic" to context.topic,
|
||||
"subtopic" to context.subtopic,
|
||||
"target_word" to context.targetWord,
|
||||
"response_mode" to context.responseMode
|
||||
)
|
||||
) ?: ""
|
||||
}
|
||||
|
||||
fun genericSmallTalk(
|
||||
context: SmallTalkContext,
|
||||
nextState: State,
|
||||
fallbackQuestion: String = "Ako sa dnes máte?",
|
||||
preparedQuestion: String? = null
|
||||
): State = state(Parent) {
|
||||
|
||||
onEntry {
|
||||
val generated = preparedQuestion ?: requestSmallTalk(context)
|
||||
val finalQuestion = if (generated.isNotBlank()) generated else fallbackQuestion
|
||||
askRepeatable(finalQuestion)
|
||||
}
|
||||
|
||||
onResponse {
|
||||
if (handleRephrase(it.intent)) return@onResponse
|
||||
// if (handleRepeat(it.intent)) return@onResponse
|
||||
|
||||
if (handleStop(it.intent, "time", "small_talk_proxy", it.text ?: "")) {
|
||||
TrainingMenuFlags.hasTrainedOnce = true
|
||||
goto(ReadyToTrain(StartQuestion, Goodbye))
|
||||
return@onResponse
|
||||
}
|
||||
|
||||
val reaction = requestSmallTalkReaction(
|
||||
userAnswer = it.text,
|
||||
robotQuestion = lastPhrase
|
||||
)
|
||||
|
||||
if (reaction != null) {
|
||||
when (reaction.type) {
|
||||
"positive" -> veryHappy()
|
||||
else -> empathy()
|
||||
}
|
||||
furhat.say(reaction.text)
|
||||
} else {
|
||||
empathy()
|
||||
furhat.say(neutralAck())
|
||||
}
|
||||
|
||||
goto(nextState)
|
||||
}
|
||||
|
||||
onNoResponse {
|
||||
goto(nextState)
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------
|
||||
// Reactions
|
||||
//----------------------------------------------------------
|
||||
data class SmallTalkReaction(
|
||||
val type: String,
|
||||
val text: String
|
||||
)
|
||||
|
||||
fun requestSmallTalkReaction(
|
||||
userAnswer: String,
|
||||
robotQuestion: String?
|
||||
): SmallTalkReaction? {
|
||||
if (!isProxyAvailable()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val raw = callProxyRespond(
|
||||
userText = userAnswer,
|
||||
task = "reaction",
|
||||
context = mapOf(
|
||||
"question" to robotQuestion,
|
||||
"user_answer" to userAnswer
|
||||
)
|
||||
) ?: return null
|
||||
|
||||
val parts = raw.split("||", limit = 2)
|
||||
if (parts.size < 2) return null
|
||||
|
||||
val type = parts[0].trim().lowercase()
|
||||
val text = parts[1].trim()
|
||||
|
||||
if (text.isBlank()) return null
|
||||
|
||||
return SmallTalkReaction(type = type, text = text)
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package furhatos.app.blank.flow.main.supporting.general
|
||||
|
||||
import furhatos.app.blank.flow.Parent
|
||||
import furhatos.app.blank.flow.main.supporting.littleSad
|
||||
import furhatos.flow.kotlin.State
|
||||
import furhatos.flow.kotlin.state
|
||||
|
||||
val Test: State = state(Parent) {
|
||||
onEntry {
|
||||
littleSad()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package furhatos.app.blank.flow.main.supporting.general
|
||||
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import kotlin.random.Random
|
||||
|
||||
data class WordEntry(
|
||||
val canonical: String,
|
||||
val theme: String,
|
||||
val difficulty: Int,
|
||||
val variants: Set<String>
|
||||
)
|
||||
|
||||
object WordBank {
|
||||
|
||||
private const val RESOURCE_PATH = "wordbank.csv"
|
||||
|
||||
val entries: List<WordEntry> by lazy { loadCsv(RESOURCE_PATH) }
|
||||
|
||||
fun pickSequence(
|
||||
length: Int,
|
||||
allowedThemes: Set<String>? = null,
|
||||
maxDifficulty: Int? = null,
|
||||
rng: Random = Random.Default
|
||||
): List<WordEntry> {
|
||||
val pool = entries.asSequence()
|
||||
.filter { allowedThemes == null || it.theme in allowedThemes }
|
||||
.filter { maxDifficulty == null || it.difficulty <= maxDifficulty }
|
||||
.distinctBy { it.canonical.lowercase() }
|
||||
.toList()
|
||||
|
||||
require(pool.size >= length) {
|
||||
"WordBank: There aren't enough words for the filters. Need to $length, there is ${pool.size}."
|
||||
}
|
||||
|
||||
return pool.shuffled(rng).take(length)
|
||||
}
|
||||
|
||||
private fun loadCsv(path: String): List<WordEntry> {
|
||||
val stream = Thread.currentThread().contextClassLoader.getResourceAsStream(path)
|
||||
?: error("WordBank: Resource is not found '$path' в src/main/resources")
|
||||
|
||||
BufferedReader(InputStreamReader(stream, Charsets.UTF_8)).use { br ->
|
||||
return br.lineSequence()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.filter { !it.startsWith("#") }
|
||||
.mapIndexed { idx, line ->
|
||||
// canonical,theme,difficulty,variants
|
||||
val parts = line.split(',', limit = 4)
|
||||
require(parts.size >= 3) {
|
||||
"WordBank CSV error (line ${idx + 1}): At least 3 fields 'canonical;theme;difficulty'"
|
||||
}
|
||||
|
||||
val canonical = parts[0].trim()
|
||||
val theme = parts[1].trim()
|
||||
val difficulty = parts[2].trim().toIntOrNull()
|
||||
?: error("WordBank CSV error (line ${idx + 1}): difficulty is not a number")
|
||||
|
||||
val variantsRaw = parts.getOrNull(3)?.trim().orEmpty()
|
||||
val variants = variantsRaw
|
||||
.split('|')
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.toMutableSet()
|
||||
|
||||
variants.add(canonical)
|
||||
|
||||
WordEntry(
|
||||
canonical = canonical,
|
||||
theme = theme,
|
||||
difficulty = difficulty,
|
||||
variants = variants
|
||||
)
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/main/kotlin/furhatos/app/blank/flow/parent.kt
Normal file
56
src/main/kotlin/furhatos/app/blank/flow/parent.kt
Normal file
@ -0,0 +1,56 @@
|
||||
package furhatos.app.blank.flow
|
||||
|
||||
import furhatos.app.blank.flow.main.handlers.Goodbye
|
||||
import furhatos.app.blank.flow.main.handlers.lastPhrase
|
||||
import furhatos.app.blank.flow.main.supporting.SmileIdle
|
||||
import furhatos.flow.kotlin.*
|
||||
import furhatos.app.blank.nlu.base_answer.Repeat
|
||||
|
||||
val Parent: State = state {
|
||||
|
||||
onEntry(inherit = true, priority = true) {
|
||||
furhat.param.noSpeechTimeout = 60_000
|
||||
furhat.param.endSilTimeout = 1_000
|
||||
furhat.param.maxSpeechTimeout = 120_000
|
||||
furhat.gesture(SmileIdle, async = true)
|
||||
propagate()
|
||||
}
|
||||
|
||||
onUserEnter(instant = true) {
|
||||
when { // "it" is the user that entered
|
||||
furhat.isAttendingUser -> furhat.glance(it) // Glance at new users entering
|
||||
!furhat.isAttendingUser -> furhat.attend(it) // Attend user if not attending anyone
|
||||
}
|
||||
}
|
||||
|
||||
onUserLeave(instant = true) {
|
||||
when {
|
||||
!users.hasAny() -> { // last user left
|
||||
goto(Goodbye)
|
||||
}
|
||||
furhat.isAttending(it) -> furhat.attend(users.other) // current user left
|
||||
!furhat.isAttending(it) -> furhat.glance(it.head.location) // other user left, just glance
|
||||
}
|
||||
}
|
||||
|
||||
onResponse<Repeat> {
|
||||
val phrase = lastPhrase
|
||||
if (phrase != null) {
|
||||
furhat.say("Samozrejme, zopakujem. ")
|
||||
furhat.ask(phrase)
|
||||
} else {
|
||||
furhat.say("Momentálne nemám čo zopakovať.")
|
||||
furhat.listen()
|
||||
}
|
||||
}
|
||||
|
||||
// onResponse<StopTraining> {
|
||||
// handleStop(it.intent)
|
||||
// }
|
||||
|
||||
onNoResponse {
|
||||
furhat.say("Je v poriadku, môžete si dať čas. Keď budete pripravení, odpovedzte, prosím.")
|
||||
reentry()
|
||||
}
|
||||
|
||||
}
|
||||
18
src/main/kotlin/furhatos/app/blank/main.kt
Normal file
18
src/main/kotlin/furhatos/app/blank/main.kt
Normal file
@ -0,0 +1,18 @@
|
||||
package furhatos.app.blank
|
||||
|
||||
import furhatos.app.blank.flow.Init
|
||||
import furhatos.flow.kotlin.Flow
|
||||
import furhatos.skills.Skill
|
||||
|
||||
class BlankSkill : Skill() {
|
||||
override fun start() {
|
||||
Flow().run(Init)
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
Skill.main(args)
|
||||
}
|
||||
|
||||
|
||||
|
||||
24
src/main/kotlin/furhatos/app/blank/nlu/AttentionTraining.kt
Normal file
24
src/main/kotlin/furhatos/app/blank/nlu/AttentionTraining.kt
Normal file
@ -0,0 +1,24 @@
|
||||
package furhatos.app.blank.nlu
|
||||
|
||||
import furhatos.nlu.Intent
|
||||
import furhatos.util.Language
|
||||
|
||||
class AttentionTraining : Intent() {
|
||||
override fun getExamples(lang: Language) = listOf(
|
||||
"pozornosť",
|
||||
"trénovať pozornosť",
|
||||
"chcem trénovať pozornosť",
|
||||
"chcem si precvičiť pozornosť",
|
||||
"chcem skúšať pozornosť",
|
||||
"chcem skúsiť pozornosť",
|
||||
"chcem otestovať pozornosť",
|
||||
"otestovať pozornosť",
|
||||
"skúšať pozornosť",
|
||||
"test pozornosti",
|
||||
"trénovať pozornosť teraz",
|
||||
"poďme na pozornosť",
|
||||
"ideme na pozornosť",
|
||||
"chcem test pozornosti",
|
||||
"chcem precvičiť sústredenie"
|
||||
)
|
||||
}
|
||||
25
src/main/kotlin/furhatos/app/blank/nlu/MemoryTraining.kt
Normal file
25
src/main/kotlin/furhatos/app/blank/nlu/MemoryTraining.kt
Normal file
@ -0,0 +1,25 @@
|
||||
package furhatos.app.blank.nlu
|
||||
|
||||
import furhatos.nlu.Intent
|
||||
import furhatos.util.Language
|
||||
|
||||
class MemoryTraining : Intent() {
|
||||
override fun getExamples(lang: Language) = listOf(
|
||||
"pamäť",
|
||||
"cvičenie na pamäť",
|
||||
"pamäťové cvičenie",
|
||||
"trénovať pamäť",
|
||||
"precvičiť pamäť",
|
||||
"chcem trénovať pamäť",
|
||||
"tréning pamäti",
|
||||
"skúsme si pamäť",
|
||||
"chcem skúsiť čas",
|
||||
"chcem si vyskúšať zapamätanie slov",
|
||||
"Ideme na pamäťové slová",
|
||||
"Môžeme začať s pamäťou?",
|
||||
"Chcem trénovať pamäť so slovami",
|
||||
"Rád by som si skúsil pamäť",
|
||||
"Rada by som si skúsila pamäť",
|
||||
"chcem otestovať pamäť"
|
||||
)
|
||||
}
|
||||
18
src/main/kotlin/furhatos/app/blank/nlu/TimeTraining.kt
Normal file
18
src/main/kotlin/furhatos/app/blank/nlu/TimeTraining.kt
Normal file
@ -0,0 +1,18 @@
|
||||
package furhatos.app.blank.nlu
|
||||
|
||||
import furhatos.nlu.Intent
|
||||
import furhatos.util.Language
|
||||
|
||||
class TimeTraining : Intent() {
|
||||
override fun getExamples(lang: Language) = listOf(
|
||||
"orientáciu v čase",
|
||||
"čas",
|
||||
"trénovať čas",
|
||||
"chcem trénovať čas",
|
||||
"chcem trénovať orientáciu v čase",
|
||||
"povedať čas",
|
||||
"chcem skúšať čas",
|
||||
"chcem skúsiť čas",
|
||||
"chcem otestovať čas"
|
||||
)
|
||||
}
|
||||
30
src/main/kotlin/furhatos/app/blank/nlu/base_answer/Ano.kt
Normal file
30
src/main/kotlin/furhatos/app/blank/nlu/base_answer/Ano.kt
Normal file
@ -0,0 +1,30 @@
|
||||
package furhatos.app.blank.nlu.base_answer
|
||||
|
||||
import furhatos.nlu.Intent
|
||||
import furhatos.util.Language
|
||||
|
||||
class Ano : Intent() {
|
||||
override fun getExamples(lang: Language): List<String> = listOf(
|
||||
"ano",
|
||||
"áno",
|
||||
"hej",
|
||||
"jasné",
|
||||
"samozrejme",
|
||||
"určite",
|
||||
"áno, prosím",
|
||||
"áno, mám",
|
||||
"ok",
|
||||
"pokračujme",
|
||||
"môžeme pokračovať",
|
||||
"poďme ďalej",
|
||||
"áno, prosím",
|
||||
"áno, môžeme",
|
||||
"áno, chcem pokračovať",
|
||||
"chcela by som pokračovať",
|
||||
"chcel by som pokračovať",
|
||||
"chcem pokračovať",
|
||||
"mám rada",
|
||||
"mám rad"
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
package furhatos.app.blank.nlu.base_answer
|
||||
|
||||
import furhatos.nlu.Intent
|
||||
import furhatos.util.Language
|
||||
|
||||
class DontKnow: Intent() {
|
||||
override fun getExamples(lang: Language) = listOf(
|
||||
"Neviem",
|
||||
"Neviem to",
|
||||
"Nie som si istý",
|
||||
"Nie som si istá",
|
||||
"Nepamätám si",
|
||||
"Nemám tušenie.",
|
||||
"Netuším",
|
||||
"Vypadlo mi to z hlavy",
|
||||
"Zabudol som",
|
||||
"Zabudola som",
|
||||
"Neviem to povedať",
|
||||
"Neviem sa rozhodnúť",
|
||||
"Bez tušenia",
|
||||
"Je to pre mňa ťažké",
|
||||
"Neviem si spomenúť"
|
||||
)
|
||||
}
|
||||
19
src/main/kotlin/furhatos/app/blank/nlu/base_answer/Help.kt
Normal file
19
src/main/kotlin/furhatos/app/blank/nlu/base_answer/Help.kt
Normal file
@ -0,0 +1,19 @@
|
||||
package furhatos.app.blank.nlu.base_answer
|
||||
|
||||
import furhatos.nlu.Intent
|
||||
import furhatos.util.Language
|
||||
|
||||
class Help: Intent() {
|
||||
override fun getExamples(lang: Language) = listOf(
|
||||
"Potrebujem pomoc",
|
||||
"Môžete mi pomôcť?",
|
||||
"Pomôž mi, prosím",
|
||||
"Prosím o pomoc",
|
||||
"Daj mi nápovedu, prosím",
|
||||
"Nemám tušenie.",
|
||||
"Netuším",
|
||||
"chcem nápovedu",
|
||||
"potrebujem nápovedu",
|
||||
"daj mi napovedu"
|
||||
)
|
||||
}
|
||||
27
src/main/kotlin/furhatos/app/blank/nlu/base_answer/Nie.kt
Normal file
27
src/main/kotlin/furhatos/app/blank/nlu/base_answer/Nie.kt
Normal file
@ -0,0 +1,27 @@
|
||||
package furhatos.app.blank.nlu.base_answer
|
||||
|
||||
import furhatos.nlu.Intent
|
||||
import furhatos.util.Language
|
||||
|
||||
class Nie : Intent() {
|
||||
override fun getExamples(lang: Language): List<String> = listOf(
|
||||
"nie",
|
||||
"nie, nemám",
|
||||
"vôbec nie",
|
||||
"ani náhodou",
|
||||
"nie, ďakujem",
|
||||
"nie, prosím",
|
||||
"nie, nechcem",
|
||||
"nechcem pokračovať",
|
||||
"nechcem ďalej",
|
||||
"už nechcem",
|
||||
"stačí",
|
||||
"to stačí",
|
||||
"pre dnešok stačí",
|
||||
"radšej nie",
|
||||
"asi nie",
|
||||
"dnes nie",
|
||||
"môžeme to ukončiť",
|
||||
"chcem skončiť"
|
||||
)
|
||||
}
|
||||
18
src/main/kotlin/furhatos/app/blank/nlu/base_answer/Repeat.kt
Normal file
18
src/main/kotlin/furhatos/app/blank/nlu/base_answer/Repeat.kt
Normal file
@ -0,0 +1,18 @@
|
||||
package furhatos.app.blank.nlu.base_answer
|
||||
|
||||
import furhatos.nlu.Intent
|
||||
import furhatos.util.Language
|
||||
|
||||
class Repeat: Intent() {
|
||||
override fun getExamples(lang: Language) = listOf(
|
||||
"môžeš to zopakovať",
|
||||
"prosím zopakuj",
|
||||
"môžeš to povedať ešte raz",
|
||||
"ešte raz prosím",
|
||||
"nepočul som ťa",
|
||||
"môžeš zopakovať otázku",
|
||||
"čo si povedal",
|
||||
"čo si povedala",
|
||||
"zopakuj"
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package furhatos.app.blank.nlu.base_answer
|
||||
|
||||
import furhatos.nlu.Intent
|
||||
import furhatos.util.Language
|
||||
|
||||
class Rephrase : Intent() {
|
||||
override fun getExamples(lang: Language)= listOf(
|
||||
"nerozumiem",
|
||||
"povedz to inak",
|
||||
"môžeš to povedať jednoduchšie",
|
||||
"povedz to prosím inak",
|
||||
"preformuluj to prosím",
|
||||
"ja som tomu nerozumel",
|
||||
"ja som tomu nerozumela",
|
||||
"skúste to povedať inak",
|
||||
"môžeš to vysvetliť jednoduchšie",
|
||||
"nepochopil som",
|
||||
"nepochopila som",
|
||||
"perifrázuj to",
|
||||
"prefrázuj to",
|
||||
"povedz to ešte inak"
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package furhatos.app.blank.nlu.base_answer
|
||||
|
||||
import furhatos.nlu.Intent
|
||||
import furhatos.util.Language
|
||||
|
||||
class StopTraining: Intent() {
|
||||
override fun getExamples(lang: Language) = listOf(
|
||||
"zastaviť",
|
||||
"zastav sa",
|
||||
"zastavme sa",
|
||||
"zastavme sa prosím",
|
||||
"stačí",
|
||||
"dosť",
|
||||
"koniec",
|
||||
"ukončiť",
|
||||
"ukonči to",
|
||||
"ukonči cvičenie",
|
||||
"skončiť",
|
||||
"skončime",
|
||||
"môžeme prestať",
|
||||
"prestať",
|
||||
"chcem prestať",
|
||||
"chcem skončiť",
|
||||
"chcem ukončiť cvičenie",
|
||||
"už nechcem pokračovať",
|
||||
"nechcem pokračovať",
|
||||
"nechcem ďalej",
|
||||
"chcela by som skončiť",
|
||||
"chcel by som skončiť",
|
||||
"skončiť",
|
||||
|
||||
// bez diakritiky
|
||||
"chcem skoncit",
|
||||
"chcem prestat",
|
||||
"ukoncit cvicenie",
|
||||
"nechcem pokracovat",
|
||||
"chcela by som skoncit",
|
||||
"chcel by som skoncit"
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package furhatos.app.blank.nlu.other_responses
|
||||
|
||||
import furhatos.util.Language
|
||||
import furhatos.nlu.Intent
|
||||
|
||||
class FeelGood : Intent() {
|
||||
override fun getExamples(lang: Language) = listOf(
|
||||
"Cítim sa dobre",
|
||||
"Cítim sa fajn",
|
||||
"Som v poriadku",
|
||||
"Je mi dobre",
|
||||
"Cítim sa výborne",
|
||||
"dobre",
|
||||
"super"
|
||||
)
|
||||
}
|
||||
|
||||
class FeelBad : Intent() {
|
||||
override fun getExamples(lang: Language) = listOf(
|
||||
"Som unavený",
|
||||
"Som unavená",
|
||||
"Cítim sa unavene",
|
||||
"Necítim sa dobre",
|
||||
"Je mi zle",
|
||||
"Nie veľmi dobré",
|
||||
"zle",
|
||||
"hrozne"
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package furhatos.app.blank.setting
|
||||
|
||||
/** Engagement parameters */
|
||||
const val MAX_NUMBER_OF_USERS = 2 // Max amount of people that Furhat will recognize as users simultaneously
|
||||
const val DISTANCE_TO_ENGAGE = 1.0 // Min distance for people to be recognised as users
|
||||
290
src/main/realtime/main.py
Normal file
290
src/main/realtime/main.py
Normal file
@ -0,0 +1,290 @@
|
||||
import os
|
||||
import asyncio
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
import pymysql
|
||||
from datetime import datetime
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
# -----------------------------
|
||||
# Config
|
||||
# -----------------------------
|
||||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||
if not OPENAI_API_KEY:
|
||||
# Лучше падать сразу, чтобы не ловить странные ошибки позже
|
||||
raise RuntimeError("Environment variable OPENAI_API_KEY is not set.")
|
||||
|
||||
# Вы можете поменять на нужную realtime-модель
|
||||
REALTIME_MODEL = os.getenv("REALTIME_MODEL", "gpt-realtime")
|
||||
|
||||
BASE_INSTRUCTIONS = os.getenv(
|
||||
"REALTIME_INSTRUCTIONS",
|
||||
# Эти инструкции НЕ должны дублировать весь ваш сценарий из Kotlin.
|
||||
# Это только общий стиль/ограничения.
|
||||
(
|
||||
"Si sociálny robot, ktorý pomáha ľuďom s kognitívno-komunikačnými problémami pomocou krátkych hier a dialógov. "
|
||||
"Vždy odpovedaj po slovensky. "
|
||||
"Odpovedaj veľmi stručne v rozsahu 1–2 viet, priateľsky a s použitím jednoduchého slovníka. "
|
||||
"Nevymýšľaj nové pravidlá hry. Ak nemáš dosť informácií, polož jednu krátku otázku. "
|
||||
),
|
||||
)
|
||||
|
||||
print("ENV KEY PRESENT:", bool(os.getenv("OPENAI_API_KEY")))
|
||||
print("MODEL:", os.getenv("REALTIME_MODEL"))
|
||||
|
||||
OUTPUT_MODALITIES = ["text"] # мы используем только текстовый realtime :contentReference[oaicite:1]{index=1}
|
||||
|
||||
MYSQL_HOST = os.getenv("MYSQL_HOST", "127.0.0.1")
|
||||
MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
|
||||
MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "furhat_logs")
|
||||
MYSQL_USER = os.getenv("MYSQL_USER", "localhost")
|
||||
MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
|
||||
|
||||
# -----------------------------
|
||||
# HTTP API models
|
||||
# -----------------------------
|
||||
class RespondRequest(BaseModel):
|
||||
# session_id: str = Field(..., description="ID логической сессии (пришлёт Kotlin).")
|
||||
# user_text: str = Field(..., description="Текст пользователя из Furhat STT / onResponse.")
|
||||
user_text: str
|
||||
# Опционально: чтобы Kotlin мог попросить конкретный тип помощи
|
||||
task: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Напр. 'hint', 'rephrase', 'explain_wrong', 'smalltalk' и т.п.",
|
||||
)
|
||||
# Опционально: краткий контекст текущего шага (вопрос, попытки и т.д.)
|
||||
context: Optional[Dict[str, Any]] = Field(default=None)
|
||||
|
||||
|
||||
class RespondResponse(BaseModel):
|
||||
answer: str
|
||||
|
||||
# -----------------------------
|
||||
# DB
|
||||
# -----------------------------
|
||||
class LogRequest(BaseModel):
|
||||
session_id: str
|
||||
game: str
|
||||
phase: str
|
||||
question: str
|
||||
attempt: int
|
||||
result: str
|
||||
user_answer: Optional[str] = None
|
||||
correct_answer: Optional[str] = None
|
||||
hint_used: bool = False
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
def get_db_connection():
|
||||
return pymysql.connect(
|
||||
host=MYSQL_HOST,
|
||||
port=MYSQL_PORT,
|
||||
user=MYSQL_USER,
|
||||
password=MYSQL_PASSWORD,
|
||||
database=MYSQL_DATABASE,
|
||||
charset="utf8mb4",
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
autocommit=True,
|
||||
)
|
||||
|
||||
# -----------------------------
|
||||
# Realtime connection manager
|
||||
# -----------------------------
|
||||
app = FastAPI(title="Realtime Proxy", version="0.1.0")
|
||||
|
||||
openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY)
|
||||
|
||||
def _build_prompt(task: Optional[str], context: Optional[Dict[str, Any]]) -> str:
|
||||
ctx = context or {}
|
||||
|
||||
if task == "hint":
|
||||
return (
|
||||
"Daj veľmi jemnú, ale konkretnú nápovedu bez toho, aby si prezradil správnu odpoveď. 1-2 vety"
|
||||
f"Kontekst: {ctx}"
|
||||
)
|
||||
if task == "rephrase":
|
||||
return (
|
||||
"Preformuluj otázku jednoduchšie, použivaj jednoduche slova, ale zachovaj význam, 1-2 vety. NEPREZRAĎ správnu odpoveď. "
|
||||
f"Kontekst: {ctx}"
|
||||
)
|
||||
if task == "explain_wrong":
|
||||
return (
|
||||
"Vysvetli stručne, PREČO odpoveď nie je správna, ale NEPREZRAĎ správnu odpoveď. "
|
||||
"Namiesto toho daj jemnú nápovedu jednou vetou (napríklad „Poplietli ste poradie slov“ alebo „Uviedli ste čas o 2 hodiny skôr, než je aktuálny“). "
|
||||
"Povzbuď na ďalší pokus. "
|
||||
f"Kontekst: {ctx}"
|
||||
)
|
||||
if task == "reaction":
|
||||
return (
|
||||
"Vyhodnoť odpoveď používateľa na small-talk otázku robota. "
|
||||
"Rozhodni iba medzi dvoma typmi reakcie: positive alebo neutral. "
|
||||
"positive použi, ak odpoveď znie príjemne, pozitívne, radostne alebo povzbudivo. "
|
||||
"neutral použi, ak je odpoveď vecná, nejasná, krátka, zmiešaná alebo ju nemožno bezpečne chápať ako pozitívnu. "
|
||||
"Potom vytvor veľmi krátku empatickú reakciu robota v jednoduchej slovenčine. "
|
||||
"Štýl má byť priateľský a úprimný, napríklad: 'To ma veľmi teší.', 'Ďakujem, že ste mi to povedali.', atď. Nepoužívaj ich stále, použivaj rôzne"
|
||||
"Nevysvetľuj svoje rozhodnutie. "
|
||||
"Výstup musí mať presne formát: typ||reakcia "
|
||||
"kde typ je iba positive alebo neutral. "
|
||||
f"Kontekst: {ctx}"
|
||||
)
|
||||
if task == "smalltalk":
|
||||
exercise = ctx.get("exercise", "")
|
||||
topic = ctx.get("topic", "")
|
||||
subtopic = ctx.get("subtopic", "")
|
||||
target_word = ctx.get("target_word", "")
|
||||
response_mode = ctx.get("response_mode", "open")
|
||||
return (
|
||||
"Polož krátku prirodzenú small-talk otázku súvisiacu s témou aktuálneho cvičenia. "
|
||||
"Použi jednoduché slová, 1-2 vety. "
|
||||
"Nemeň tému a nevymýšľaj nové pravidlá hry. "
|
||||
"Nevypisuj viac otázok naraz. "
|
||||
f"Typ cvičenia: {exercise}. "
|
||||
f"Téma: {topic}. "
|
||||
f"Podtéma: {subtopic}. "
|
||||
f"Kľúčové slovo: {target_word}. "
|
||||
f"Formát odpovede používateľa: {response_mode}. "
|
||||
"Ak je formát 'yes_no', vytvor otázku, na ktorú sa prirodzene odpovedá áno alebo nie. "
|
||||
"Ak je formát 'open', vytvor otvorenú každodennú otázku. "
|
||||
)
|
||||
|
||||
# default
|
||||
return f"Odpovedaj stručne a vecne. Kontekst: {ctx}"
|
||||
|
||||
|
||||
async def _ask_realtime(user_text: str, task: Optional[str], context: Optional[Dict[str, Any]]) -> str:
|
||||
per_turn_instructions = _build_prompt(task, context)
|
||||
|
||||
async with openai_client.realtime.connect(model=REALTIME_MODEL) as conn:
|
||||
await conn.session.update(
|
||||
session={
|
||||
"type": "realtime",
|
||||
"output_modalities": ["text"],
|
||||
"instructions": BASE_INSTRUCTIONS,
|
||||
"tool_choice": "none",
|
||||
"tools": [],
|
||||
"audio": {"input": {"turn_detection": None}},
|
||||
}
|
||||
)
|
||||
|
||||
await conn.conversation.item.create(
|
||||
item={
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": user_text}],
|
||||
}
|
||||
)
|
||||
|
||||
await conn.response.create(
|
||||
response={
|
||||
"instructions": per_turn_instructions,
|
||||
"max_output_tokens": 70,
|
||||
}
|
||||
)
|
||||
|
||||
chunks = []
|
||||
|
||||
async for event in conn:
|
||||
# print("EV:", event.type)
|
||||
|
||||
if event.type in ("response.output_text.delta", "response.text.delta"):
|
||||
chunks.append(event.delta)
|
||||
|
||||
elif event.type == "response.done":
|
||||
# ЛОГИ — всегда, чтобы видеть, что реально пришло в DONE
|
||||
resp = getattr(event, "response", None)
|
||||
output = getattr(resp, "output", None) or []
|
||||
# print("DONE output len:", len(output))
|
||||
# print("DONE output raw:", output)
|
||||
# print("DONE response obj:", resp)
|
||||
|
||||
# Если дельт не было — пробуем достать финальный текст из response.output
|
||||
if not chunks:
|
||||
for item in output:
|
||||
if getattr(item, "type", None) == "message":
|
||||
for c in getattr(item, "content", []) or []:
|
||||
ctype = getattr(c, "type", None)
|
||||
if ctype in ("output_text", "text"):
|
||||
txt = getattr(c, "text", "") or ""
|
||||
if txt:
|
||||
chunks.append(txt)
|
||||
|
||||
break
|
||||
|
||||
elif event.type == "error":
|
||||
raise RuntimeError(f"Realtime error: {event.error}")
|
||||
|
||||
answer = "".join(chunks).strip()
|
||||
return answer or "Prepáčte, môžete to prosím zopakovať?"
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# FastAPI endpoints
|
||||
# -----------------------------
|
||||
@app.get("/test")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.post("/respond", response_model=RespondResponse)
|
||||
async def respond(req: RespondRequest):
|
||||
try:
|
||||
answer = await _ask_realtime(req.user_text, req.task, req.context)
|
||||
print("RESPOND:", req.user_text, req.task)
|
||||
return RespondResponse(answer=answer)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/reset")
|
||||
async def reset():
|
||||
print("RESET called")
|
||||
return {"status": "reset"}
|
||||
|
||||
@app.post("/log")
|
||||
async def log_event(req: LogRequest):
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO session_logs (
|
||||
session_id,
|
||||
game,
|
||||
phase,
|
||||
question,
|
||||
attempt,
|
||||
result,
|
||||
user_answer,
|
||||
correct_answer,
|
||||
hint_used,
|
||||
created_at
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
req.session_id,
|
||||
req.game,
|
||||
req.phase,
|
||||
req.question,
|
||||
req.attempt,
|
||||
req.result,
|
||||
req.user_answer,
|
||||
req.correct_answer,
|
||||
req.hint_used,
|
||||
req.created_at or datetime.utcnow(),
|
||||
)
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
except Exception as e:
|
||||
print("LOG ERROR:", repr(e))
|
||||
traceback.print_exc()
|
||||
raise HTTPException(status_code=500, detail=f"log insert failed: {str(e)}")
|
||||
4
src/main/realtime/requirements.txt
Normal file
4
src/main/realtime/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
openai
|
||||
python-dotenv
|
||||
159
src/main/resources/wordbank.csv
Normal file
159
src/main/resources/wordbank.csv
Normal file
@ -0,0 +1,159 @@
|
||||
|
||||
jablko,ovocie,1,jablka|jablk|jablky
|
||||
hruška,ovocie,1,hruska|hruška|hrušk
|
||||
banán,ovocie,1,banan|banán|banana
|
||||
pomaranč,ovocie,2,pomaranc|pomaranče
|
||||
citrón,ovocie,2,citron|citróny|citróne
|
||||
jahoda,bobuľa,1,jahoda|jahod|jahda
|
||||
malina,bobuľa,2,malina|maliny
|
||||
orech,jedlo,1,orech
|
||||
chlieb,jedlo,1,chlieb
|
||||
maslo,jedlo,1,maslo
|
||||
syr,jedlo,1,syr
|
||||
mlieko,nápoje,1,mlieko
|
||||
voda,nápoje,1,voda
|
||||
čaj,nápoje,1,caj|čaj
|
||||
káva,nápoje,1,kava|káva
|
||||
polievka,jedlo,2,polievka
|
||||
ryža,jedlo,2,ryza|ryža
|
||||
cestovina,jedlo,2,cestovina
|
||||
mäso,jedlo,1,maso|mäso
|
||||
kurča,jedlo,2,kurca|kurča
|
||||
ryba,jedlo,1,ryba
|
||||
vajce,jedlo,1,vajce
|
||||
soľ,prísady,1,sol|soľ
|
||||
cukor,prísady,1,cukor
|
||||
med,sladké,1,med
|
||||
jogurt,jedlo,2,jogurt
|
||||
koláč,sladké,2,kolac|koláč
|
||||
čokoláda,sladké,2,cokolada|čokoláda
|
||||
zmrzlina,sladké,2,zmrzlina
|
||||
paradajka,zelenina,1,paradajka
|
||||
uhorka,zelenina,1,uhorka
|
||||
zemiak,zelenina,1,zemiaky
|
||||
mrkva,zelenina,1,mrkva
|
||||
cibuľa,zelenina,2,cibula|cibuľa
|
||||
cesnak,zelenina,1,cesnak
|
||||
paprika,zelenina,1,paprika
|
||||
šalát,zelenina,2,salat|šalát
|
||||
fazuľa,zelenina,2,fazula|fazuľa
|
||||
hrášok,zelenina,2,hrasok|hrášok
|
||||
múka,jedlo,2,muka|múka
|
||||
ocot,prísady,2,ocot
|
||||
stôl,domáce potreby,1,stol|stôl
|
||||
stolička,domáce potreby,1,stolicka|stolička
|
||||
posteľ,domáce potreby,2,postel|posteľ
|
||||
vankúš,domáce potreby,2,vankus|vankúš
|
||||
deka,domáce potreby,1,deka
|
||||
skriňa,domáce potreby,2,skrina|skriňa
|
||||
dvere,dom,1,dvere
|
||||
okno,dom,1,okno
|
||||
lampa,technika,1,lampa
|
||||
koberec,domáce potreby,2,koberec
|
||||
zrkadlo,domáce potreby,2,zrkadlo
|
||||
kľúč,dom,1,kluc|kľúč
|
||||
zásuvka,dom,2,zasuvka|zásuvka
|
||||
polica,dom,1,polica
|
||||
chladnička,dom,3,chladnicka|chladnička
|
||||
rúra,dom,2,rura|rúra
|
||||
sporák,dom,3,sporak|sporák
|
||||
umývadlo,hygiena,3,umyvadlo|umývadlo
|
||||
sprcha,hygiena,2,sprcha
|
||||
vaňa,dom,2,vana|vaňa
|
||||
uterák,dom,2,uterak|uterák
|
||||
mydlo,hygiena,1,mydlo
|
||||
šampón,hygiena,3,sampón|sampon|šampón
|
||||
práčka,technika,3,pracka|práčka
|
||||
žehlička,technika,3,zehlicka|žehlička
|
||||
televízor,druh zábavy,3,televizor|televízor
|
||||
rádio,technika,2,radio|rádio
|
||||
počítač,technika,3,pocitac|počítač
|
||||
telefón,technika,2,telefon|telefón
|
||||
hodiny,technika,2,hodiny
|
||||
vypínač,technika,3,vypinac|vypínač
|
||||
batéria,technika,2,bateria|batéria
|
||||
strom,príroda,1,strom
|
||||
kvet,príroda,1,kvet
|
||||
tráva,príroda,1,trava|tráva
|
||||
list,príroda,1,list
|
||||
vetva,príroda,1,vetva
|
||||
rieka,príroda,1,rieka
|
||||
jazero,príroda,2,jazero
|
||||
more,príroda,1,more
|
||||
hora,príroda,1,hora
|
||||
les,príroda,1,les
|
||||
lúka,príroda,2,luka|lúka
|
||||
dážď,príroda,2,dazd|dážď
|
||||
sneh,príroda,1,sneh
|
||||
vietor,príroda,1,vietor
|
||||
slnko,príroda,1,slnko
|
||||
oblak,príroda,1,oblak
|
||||
hviezda,príroda,2,hviezda
|
||||
mesiac,príroda,1,mesiac
|
||||
kameň,príroda,2,kamen|kameň
|
||||
piesok,príroda,1,piesok
|
||||
pôda,príroda,2,poda|pôda
|
||||
oheň,príroda,2,ohen|oheň
|
||||
plameň,príroda,3,plamen|plameň
|
||||
vlna,príroda,2,vlna
|
||||
hmla,príroda,2,hmla
|
||||
ulica,mesto,1,ulica
|
||||
námestie,mesto,2,namestie|námestie
|
||||
škola,mesto,1,skola|škola
|
||||
obchod,mesto,1,obchod
|
||||
trh,mesto,2,trh
|
||||
banka,mesto,1,banka
|
||||
pošta,mesto,2,posta|pošta
|
||||
nemocnica,mesto,3,nemocnica
|
||||
lekáreň,mesto,3,lekaren|lekáreň
|
||||
stanica,mesto,2,stanica
|
||||
zastávka,mesto,2,zastavka|zastávka
|
||||
autobus,mesto,1,autobus
|
||||
vlak,mesto,1,vlak
|
||||
taxi,mesto,2,taxi
|
||||
most,mesto,1,most
|
||||
park,mesto,1,park
|
||||
kino,druh zábavy,1,kino
|
||||
divadlo,druh zábavy,2,divadlo
|
||||
reštaurácia,mesto,3,restauracia|reštaurácia
|
||||
kaviareň,mesto,3,kavieren|kaviareň
|
||||
hotel,mesto,2,hotel
|
||||
múzeum,druh zábavy,3,muzeum|múzeum
|
||||
knižnica,mesto,3,kniznica|knižnica
|
||||
polícia,mesto,3,policia|polícia
|
||||
hlava,telo,1,hlava
|
||||
oko,telo,1,oko
|
||||
ucho,telo,1,ucho
|
||||
nos,telo,1,nos
|
||||
ústa,telo,2,usta|ústa
|
||||
zub,telo,1,zub
|
||||
jazyk,telo,2,jazyk
|
||||
krk,telo,1,krk
|
||||
rameno,telo,2,rameno
|
||||
ruka,telo,1,ruka
|
||||
dlaň,telo,2,dlan|dlaň
|
||||
prst,telo,1,prst
|
||||
lakeť,telo,3,laket|lakeť
|
||||
chrbát,telo,3,chrbat|chrbát
|
||||
brucho,telo,1,brucho
|
||||
noha,telo,1,noha
|
||||
koleno,telo,1,koleno
|
||||
členok,telo,3,clenok|členok
|
||||
päta,telo,2,peta|päta
|
||||
srdce,telo,2,srdce
|
||||
pľúca,telo,3,pluca|pľúca
|
||||
žalúdok,telo,3,zaludok|žalúdok
|
||||
pečeň,telo,3,pecen|pečeň
|
||||
tričko,oblečenie,2,tricko|tričko
|
||||
nohavice,oblečenie,1,nohavice
|
||||
sukňa,oblečenie,2,sukna|sukňa
|
||||
šaty,oblečenie,1,saty|šaty
|
||||
sveter,oblečenie,2,sveter
|
||||
bunda,oblečenie,1,bunda
|
||||
kabát,oblečenie,2,kabat|kabát
|
||||
topánka,oblečenie,2,topanka|topánka
|
||||
ponožka,oblečenie,2,ponozka|ponožka
|
||||
čiapka,oblečenie,2,ciapka|čiapka
|
||||
šál,oblečenie,2,sal|šál
|
||||
rukavica,oblečenie,2,rukavica
|
||||
opasok,oblečenie,2,opasok
|
||||
|
Loading…
Reference in New Issue
Block a user