add comms

This commit is contained in:
val966 2026-05-18 15:01:11 +02:00
commit 1395669d67
59 changed files with 6596 additions and 0 deletions

27
.gitignore vendored Normal file
View 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
View 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.

File diff suppressed because one or more lines are too long

View 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
View 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;
/***/ })
/******/ ]);

View 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
View 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

Binary file not shown.

View 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
View 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
View 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
View 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

View 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)
}
}

View 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."
}
}

View File

@ -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)
}
}
}
}

View 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()
}
}

View File

@ -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)
}
}

View 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)
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View 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()
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View 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)
}
}

View 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()
}

View File

@ -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"
}
}

View File

@ -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 "511",
"deň" to "1217",
"večer" to "1823"
)
}
}
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)
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()
}
}

View File

@ -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()
}
}
}

View 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()
}
}

View 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)
}

View 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"
)
}

View 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äť"
)
}

View 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"
)
}

View 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"
)
}

View File

@ -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úť"
)
}

View 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"
)
}

View 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ť"
)
}

View 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"
)
}

View File

@ -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"
)
}

View File

@ -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"
)
}

View File

@ -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"
)
}

View File

@ -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
View 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 12 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)}")

View File

@ -0,0 +1,4 @@
fastapi
uvicorn[standard]
openai
python-dotenv

View 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
1 jablko ovocie 1 jablka|jablk|jablky
2 hruška ovocie 1 hruska|hruška|hrušk
3 banán ovocie 1 banan|banán|banana
4 pomaranč ovocie 2 pomaranc|pomaranče
5 citrón ovocie 2 citron|citróny|citróne
6 jahoda bobuľa 1 jahoda|jahod|jahda
7 malina bobuľa 2 malina|maliny
8 orech jedlo 1 orech
9 chlieb jedlo 1 chlieb
10 maslo jedlo 1 maslo
11 syr jedlo 1 syr
12 mlieko nápoje 1 mlieko
13 voda nápoje 1 voda
14 čaj nápoje 1 caj|čaj
15 káva nápoje 1 kava|káva
16 polievka jedlo 2 polievka
17 ryža jedlo 2 ryza|ryža
18 cestovina jedlo 2 cestovina
19 mäso jedlo 1 maso|mäso
20 kurča jedlo 2 kurca|kurča
21 ryba jedlo 1 ryba
22 vajce jedlo 1 vajce
23 soľ prísady 1 sol|soľ
24 cukor prísady 1 cukor
25 med sladké 1 med
26 jogurt jedlo 2 jogurt
27 koláč sladké 2 kolac|koláč
28 čokoláda sladké 2 cokolada|čokoláda
29 zmrzlina sladké 2 zmrzlina
30 paradajka zelenina 1 paradajka
31 uhorka zelenina 1 uhorka
32 zemiak zelenina 1 zemiaky
33 mrkva zelenina 1 mrkva
34 cibuľa zelenina 2 cibula|cibuľa
35 cesnak zelenina 1 cesnak
36 paprika zelenina 1 paprika
37 šalát zelenina 2 salat|šalát
38 fazuľa zelenina 2 fazula|fazuľa
39 hrášok zelenina 2 hrasok|hrášok
40 múka jedlo 2 muka|múka
41 ocot prísady 2 ocot
42 stôl domáce potreby 1 stol|stôl
43 stolička domáce potreby 1 stolicka|stolička
44 posteľ domáce potreby 2 postel|posteľ
45 vankúš domáce potreby 2 vankus|vankúš
46 deka domáce potreby 1 deka
47 skriňa domáce potreby 2 skrina|skriňa
48 dvere dom 1 dvere
49 okno dom 1 okno
50 lampa technika 1 lampa
51 koberec domáce potreby 2 koberec
52 zrkadlo domáce potreby 2 zrkadlo
53 kľúč dom 1 kluc|kľúč
54 zásuvka dom 2 zasuvka|zásuvka
55 polica dom 1 polica
56 chladnička dom 3 chladnicka|chladnička
57 rúra dom 2 rura|rúra
58 sporák dom 3 sporak|sporák
59 umývadlo hygiena 3 umyvadlo|umývadlo
60 sprcha hygiena 2 sprcha
61 vaňa dom 2 vana|vaňa
62 uterák dom 2 uterak|uterák
63 mydlo hygiena 1 mydlo
64 šampón hygiena 3 sampón|sampon|šampón
65 práčka technika 3 pracka|práčka
66 žehlička technika 3 zehlicka|žehlička
67 televízor druh zábavy 3 televizor|televízor
68 rádio technika 2 radio|rádio
69 počítač technika 3 pocitac|počítač
70 telefón technika 2 telefon|telefón
71 hodiny technika 2 hodiny
72 vypínač technika 3 vypinac|vypínač
73 batéria technika 2 bateria|batéria
74 strom príroda 1 strom
75 kvet príroda 1 kvet
76 tráva príroda 1 trava|tráva
77 list príroda 1 list
78 vetva príroda 1 vetva
79 rieka príroda 1 rieka
80 jazero príroda 2 jazero
81 more príroda 1 more
82 hora príroda 1 hora
83 les príroda 1 les
84 lúka príroda 2 luka|lúka
85 dážď príroda 2 dazd|dážď
86 sneh príroda 1 sneh
87 vietor príroda 1 vietor
88 slnko príroda 1 slnko
89 oblak príroda 1 oblak
90 hviezda príroda 2 hviezda
91 mesiac príroda 1 mesiac
92 kameň príroda 2 kamen|kameň
93 piesok príroda 1 piesok
94 pôda príroda 2 poda|pôda
95 oheň príroda 2 ohen|oheň
96 plameň príroda 3 plamen|plameň
97 vlna príroda 2 vlna
98 hmla príroda 2 hmla
99 ulica mesto 1 ulica
100 námestie mesto 2 namestie|námestie
101 škola mesto 1 skola|škola
102 obchod mesto 1 obchod
103 trh mesto 2 trh
104 banka mesto 1 banka
105 pošta mesto 2 posta|pošta
106 nemocnica mesto 3 nemocnica
107 lekáreň mesto 3 lekaren|lekáreň
108 stanica mesto 2 stanica
109 zastávka mesto 2 zastavka|zastávka
110 autobus mesto 1 autobus
111 vlak mesto 1 vlak
112 taxi mesto 2 taxi
113 most mesto 1 most
114 park mesto 1 park
115 kino druh zábavy 1 kino
116 divadlo druh zábavy 2 divadlo
117 reštaurácia mesto 3 restauracia|reštaurácia
118 kaviareň mesto 3 kavieren|kaviareň
119 hotel mesto 2 hotel
120 múzeum druh zábavy 3 muzeum|múzeum
121 knižnica mesto 3 kniznica|knižnica
122 polícia mesto 3 policia|polícia
123 hlava telo 1 hlava
124 oko telo 1 oko
125 ucho telo 1 ucho
126 nos telo 1 nos
127 ústa telo 2 usta|ústa
128 zub telo 1 zub
129 jazyk telo 2 jazyk
130 krk telo 1 krk
131 rameno telo 2 rameno
132 ruka telo 1 ruka
133 dlaň telo 2 dlan|dlaň
134 prst telo 1 prst
135 lakeť telo 3 laket|lakeť
136 chrbát telo 3 chrbat|chrbát
137 brucho telo 1 brucho
138 noha telo 1 noha
139 koleno telo 1 koleno
140 členok telo 3 clenok|členok
141 päta telo 2 peta|päta
142 srdce telo 2 srdce
143 pľúca telo 3 pluca|pľúca
144 žalúdok telo 3 zaludok|žalúdok
145 pečeň telo 3 pecen|pečeň
146 tričko oblečenie 2 tricko|tričko
147 nohavice oblečenie 1 nohavice
148 sukňa oblečenie 2 sukna|sukňa
149 šaty oblečenie 1 saty|šaty
150 sveter oblečenie 2 sveter
151 bunda oblečenie 1 bunda
152 kabát oblečenie 2 kabat|kabát
153 topánka oblečenie 2 topanka|topánka
154 ponožka oblečenie 2 ponozka|ponožka
155 čiapka oblečenie 2 ciapka|čiapka
156 šál oblečenie 2 sal|šál
157 rukavica oblečenie 2 rukavica
158 opasok oblečenie 2 opasok