blob: 216cc198e76ec8f1056f70e361cd5510ad2d0ef7 [file] [log] [blame]
package controllers
import io.prediction.commons.Config
import io.prediction.commons.settings._
import io.prediction.commons.modeldata.ItemRecScores
import io.prediction.commons.appdata.{ Users, Items, U2IActions }
import io.prediction.output.AlgoOutputSelector
import Helper.{ algoToJson, offlineEvalMetricToJson }
import Helper.{ dateTimeToString, algoParamToString, offlineEvalSplitterParamToString }
import Helper.{ getSimEvalStatus, getOfflineTuneStatus, createOfflineEval }
import play.api._
import play.api.mvc._
import play.api.data._
import play.api.data.Forms._
import play.api.data.format.Formats._
import play.api.data.validation.{ Constraints }
import play.api.i18n.{ Messages, Lang }
import play.api.libs.concurrent.Execution.Implicits._
import play.api.libs.json.Json.toJson
import play.api.libs.json.{ JsNull, JsArray, Json, JsValue, Writes, JsObject }
import play.api.libs.ws.WS
import play.api.Play.current
import play.api.http
import scala.concurrent.{ Await, Future }
import scala.concurrent.duration._
import scala.util.Random
import com.github.nscala_time.time.Imports._
import org.apache.commons.codec.digest.DigestUtils
import Forms._
/*
* TODO:
* - decodeURIComponent any GET custom param
*/
/*
* Backend of ControlPanel.
* Pure REST APIs in JSON.
*/
object Application extends Controller {
/** PredictionIO Commons settings*/
val config = new Config()
val users = config.getSettingsUsers()
val apps = config.getSettingsApps()
val engines = config.getSettingsEngines()
val engineInfos = config.getSettingsEngineInfos()
val algos = config.getSettingsAlgos()
val algoInfos = config.getSettingsAlgoInfos()
val offlineEvalMetricInfos = config.getSettingsOfflineEvalMetricInfos()
val offlineEvalSplitterInfos = config.getSettingsOfflineEvalSplitterInfos()
val offlineEvals = config.getSettingsOfflineEvals()
val offlineEvalMetrics = config.getSettingsOfflineEvalMetrics()
val offlineEvalResults = config.getSettingsOfflineEvalResults()
val offlineEvalSplitters = config.getSettingsOfflineEvalSplitters()
val offlineTunes = config.getSettingsOfflineTunes()
val paramGens = config.getSettingsParamGens()
/** PredictionIO Commons modeldata */
val itemRecScores = config.getModeldataItemRecScores()
val itemSimScores = config.getModeldataItemSimScores()
/** PredictionIO Commons modeldata */
val trainingItemRecScores = config.getModeldataTrainingItemRecScores()
val trainingItemSimScores = config.getModeldataTrainingItemSimScores()
/** PredictionIO Commons appdata */
val appDataUsers = config.getAppdataUsers()
val appDataItems = config.getAppdataItems()
val appDataU2IActions = config.getAppdataU2IActions()
/** PredictionIO Commons training set appdata */
val trainingSetUsers = config.getAppdataTrainingUsers()
val trainingSetItems = config.getAppdataTrainingItems()
val trainingSetU2IActions = config.getAppdataTrainingU2IActions()
/** PredictionIO Commons validation set appdata */
val validationSetUsers = config.getAppdataValidationUsers()
val validationSetItems = config.getAppdataValidationItems()
val validationSetU2IActions = config.getAppdataValidationU2IActions()
/** PredictionIO Commons test set appdata */
val testSetUsers = config.getAppdataTestUsers()
val testSetItems = config.getAppdataTestItems()
val testSetU2IActions = config.getAppdataTestU2IActions()
/** Scheduler setting */
val settingsSchedulerUrl = config.settingsSchedulerUrl
/** PredictionIO Output */
val algoOutputSelector = new AlgoOutputSelector(algos)
/** misc */
val nameRegex = """\b[a-zA-Z][a-zA-Z0-9_-]*\b""".r
/** Play Framework security */
def username(request: RequestHeader) = request.session.get(Security.username)
def onUnauthorized(request: RequestHeader) = Forbidden(toJson(Map("message" -> toJson("Haven't signed in yet."))))
def withAuth(f: => String => Request[AnyContent] => Result) = {
Security.Authenticated(username, onUnauthorized) { user =>
Action(request => f(user)(request))
}
}
def withAuthAsync(f: => String => Request[AnyContent] => Future[SimpleResult]) = {
Security.Authenticated(username, onUnauthorized) { user =>
Action.async(request => f(user)(request))
}
}
object WithUser {
def apply(f: User => Request[AnyContent] => SimpleResult) = async { user =>
implicit request =>
Future.successful(f(user)(request))
}
def async(f: User => Request[AnyContent] => Future[SimpleResult]) = withAuthAsync { username =>
implicit request =>
users.getByEmail(username).map { user =>
f(user)(request)
}.getOrElse(Future.successful(onUnauthorized(request)))
}
}
object WithApp {
def apply(appid: Int)(f: (User, App) => Request[AnyContent] => SimpleResult) = async(appid) { (user, app) =>
implicit request =>
Future.successful(f(user, app)(request))
}
def async(appid: Int)(f: (User, App) => Request[AnyContent] => Future[SimpleResult]) = WithUser.async { user =>
implicit request =>
apps.getByIdAndUserid(appid, user.id).map { app =>
f(user, app)(request)
}.getOrElse(Future.successful(NotFound(Json.obj("message" -> s"Invalid appid ${appid}."))))
}
}
object WithEngine {
def apply(appid: Int, engineid: Int)(f: (User, App, Engine) => Request[AnyContent] => SimpleResult) = async(appid, engineid) {
(user, app, eng) =>
implicit request =>
Future.successful(f(user, app, eng)(request))
}
def async(appid: Int, engineid: Int)(f: (User, App, Engine) => Request[AnyContent] => Future[SimpleResult]) = WithApp.async(appid) {
(user, app) =>
implicit request =>
engines.getByIdAndAppid(engineid, appid).map { eng =>
f(user, app, eng)(request)
}.getOrElse(Future.successful(NotFound(Json.obj("message" -> s"Invalid engineid ${engineid}."))))
}
}
object WithAlgo {
def apply(appid: Int, engineid: Int, algoid: Int)(f: (User, App, Engine, Algo) => Request[AnyContent] => SimpleResult) = async(appid, engineid, algoid) {
(user, app, eng, algo) =>
implicit request =>
Future.successful(f(user, app, eng, algo)(request))
}
def async(appid: Int, engineid: Int, algoid: Int)(f: (User, App, Engine, Algo) => Request[AnyContent] => Future[SimpleResult]) = WithEngine.async(appid, engineid) {
(user, app, eng) =>
implicit request =>
algos.getByIdAndEngineid(algoid, engineid).map { algo =>
f(user, app, eng, algo)(request)
}.getOrElse(Future.successful(NotFound(Json.obj("message" -> s"Invalid algoid ${algoid}."))))
}
}
object WithOfflineEval {
def apply(appid: Int, engineid: Int, offlineevalid: Int)(f: (User, App, Engine, OfflineEval) => Request[AnyContent] => SimpleResult) = async(appid, engineid, offlineevalid) {
(user, app, eng, eval) =>
implicit request =>
Future.successful(f(user, app, eng, eval)(request))
}
def async(appid: Int, engineid: Int, offlineevalid: Int)(f: (User, App, Engine, OfflineEval) => Request[AnyContent] => Future[SimpleResult]) = WithEngine.async(appid, engineid) {
(user, app, eng) =>
implicit request =>
offlineEvals.getByIdAndEngineid(offlineevalid, engineid).map { eval =>
f(user, app, eng, eval)(request)
}.getOrElse(Future.successful(NotFound(Json.obj("message" -> s"Invalid offlineevalid ${offlineevalid}."))))
}
}
private def md5password(password: String) = DigestUtils.md5Hex(password)
/** Appkey Generation */
def randomAlphanumeric(n: Int): String = {
Random.alphanumeric.take(n).mkString
}
def showWeb() = Action {
Ok(views.html.Web.index())
}
/* Serve Engines/Algorithms Static Files (avoid PlayFramework's Assets cache problem during development)*/
def enginebase(path: String) = Action {
Ok.sendFile(new java.io.File(Play.application.path, "/enginebase/" + path))
//TODO: Fix Content-Disposition
}
def redirectToWeb = Action {
Redirect("web/")
}
/* case class to json conversion */
implicit val userWrites = new Writes[User] {
/* note: do not return password */
def writes(u: User): JsValue = {
Json.obj(
"id" -> u.id,
"username" -> (u.firstName + u.lastName.map(" " + _).getOrElse("")),
"email" -> u.email
)
}
}
/**
* Authenticates user
*
* {{{
* POST
* JSON parameters:
* {
* "email" : <string>,
* "password" : <string>,
* "remember" : <optional string "on">
* }
* JSON response:
* {
* "id" : <string>,
* "username" : <string>,
* "email" : <string>
* }
* }}}
*
*/
def signin = Action { implicit request =>
val loginForm = Form(
tuple(
"email" -> text,
"password" -> text,
"remember" -> optional(text)
) verifying ("Invalid email or password", result => result match {
case (email, password, remember) => users.authenticateByEmail(email, md5password(password)) map { _ => true } getOrElse false
})
)
loginForm.bindFromRequest.fold(
formWithErrors => Forbidden(toJson(Map("message" -> toJson("Incorrect Email or Password.")))),
form => {
users.getByEmail(form._1).map(user =>
Ok(Json.toJson(user)).withSession(Security.username -> user.email)
).getOrElse(
InternalServerError(Json.obj("message" -> "Could not find your user account."))
)
}
)
}
/**
* Signs out
*
* {{{
* POST
* JSON parameters:
* None
* JSON response:
* None
* }}}
*/
def signout = Action {
Ok.withNewSession
}
/**
* Returns authenticated user info
* (from session cookie)
*
* {{{
* GET
* JSON parameters:
* None
* JSON response:
* If authenticated
* OK
* {
* "id" : <string>,
* "username" : <string>,
* "email": <string>
* }
*
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
* }}}
*/
def getAuth = WithUser { user =>
implicit request =>
Ok(Json.toJson(user))
}
/**
* Returns list of apps of the authenticated user
*
* {{{
* GET
* JSON parameters:
* None
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If no app:
* No Content
*
* If apps are found:
* OK
* [ { "id" : <appid int>, "appname" : <string> },
* ...
* ]
*
* }}}
*/
def getApplist = WithUser { user =>
implicit request =>
val userApps = apps.getByUserid(user.id)
if (!userApps.hasNext) NoContent
else {
Ok(JsArray(userApps.map { app =>
Json.obj("id" -> app.id, "appname" -> app.display)
}.toSeq))
}
}
/**
* Returns the app details of this appid
*
* {{{
* GET
* JSON parameters:
* None
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If app is not found:
* NotFound
* {
* "message" : "Invalid appid."
* }
*
* If app is found:
* Ok
* {
* "id" : <appid int>,
* "updatedtime" : <string>,
* "userscount": <num of users int>
* "itemscount": <num of items int>
* "u2icount": <num of u2i Actions int>
* "apiurl": <url of API server, string>
* "appkey": <string>
* }
* }}}
*
* @param id the App ID
*/
def getAppDetails(id: Int) = WithApp(id) { (user, app) =>
implicit request =>
val numUsers = appDataUsers.countByAppid(app.id)
val numItems = appDataItems.countByAppid(app.id)
val numU2IActions = appDataU2IActions.countByAppid(app.id)
Ok(Json.obj(
"id" -> app.id,
"updatedtime" -> dateTimeToString(DateTime.now),
"userscount" -> numUsers,
"itemscount" -> numItems,
"u2icount" -> numU2IActions,
"apiurl" -> "http://yourhost.com:123/appid12",
"appkey" -> app.appkey
))
}
/**
* Returns an app
*
* {{{
* GET
* JSON parameters:
* None
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If app is found:
* Ok
* {
* "id" : <appid int>
* "appname" : <string>
* }
*
* }}}
*
* @param id the App ID
*/
def getApp(id: Int) = WithApp(id) { (user, app) =>
implicit request =>
Ok(Json.obj(
"id" -> app.id,
"appname" -> app.display
))
}
/**
* Create an app
*
* {{{
* POST
* JSON Parameters:
* {
* "appname" : <the new app name. string>
* }
* JSON Response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If creation failed:
* BadRequest
* {
* "message" : "Invalid character for app name."
* }
*
* If app is created:
* OK
* {
* "id" : <the appid of the created app. int>,
* "appname" : <string>
* }
* }}}
*
*/
def createApp = WithUser { user =>
implicit request =>
val appForm = Form(single(
"appname" -> nonEmptyText
))
appForm.bindFromRequest.fold(
formWithError => {
val msg = formWithError.errors(0).message // extract 1st error message only
BadRequest(toJson(Map("message" -> toJson(msg))))
},
formData => {
val an = formData
val appid = apps.insert(App(
id = 0,
userid = user.id,
appkey = randomAlphanumeric(64),
display = an,
url = None,
cat = None,
desc = None,
timezone = "UTC"
))
Logger.info("Create app ID " + appid)
Ok(Json.obj(
"id" -> appid,
"appname" -> an
))
}
)
}
/**
* Remove an app
*
* {{{
* DELETE
* JSON Parameters:
* None
* JSON Response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If not found:
* NotFound
* {
* "message" : <error message>
* }
*
* If error:
* InternalServerError
* {
* "message" : <error message>
* }
*
* If deleted successfully:
* Ok
* }}}
*
* @param id the App ID
*/
def removeApp(id: Int) = WithApp.async(id) { (user, app) =>
implicit request =>
// don't delete if there is any deployed aglo, sim eval or offline tune pending
val appEngines = engines.getByAppid(app.id).toList
val enginesDeployed = appEngines.filter(eng =>
!(algos.getDeployedByEngineid(eng.id).isEmpty)
)
val msgDeployed = "There are deployed algorithms in engines: " + enginesDeployed.map(_.name).mkString(", ") + ". Please undeploy them before delete this app."
val enginesSimEvals = appEngines.filter(eng =>
!(Helper.getSimEvalsByEngineid(eng.id).filter(Helper.isPendingSimEval(_)).isEmpty)
)
val msgSimEvals = "There are running simulated evaluations in engines: " + enginesSimEvals.map(_.name).mkString(", ") + ". Please stop and delete them before delete this app."
val enginesOfflineTunes = appEngines.filter(eng =>
!(offlineTunes.getByEngineid(eng.id).filter(Helper.isPendingOfflineTune(_)).isEmpty)
)
val msgOfflineTunes = "There are auto-tuning algorithms in engines: " + enginesOfflineTunes.map(_.name).mkString(", ") + ". Please stop and delete them before delete this app."
val runningEngines = List(
(enginesDeployed, msgDeployed),
(enginesSimEvals, msgSimEvals),
(enginesOfflineTunes, msgOfflineTunes)
).filter { case (x, y) => (!x.isEmpty) }
if (!runningEngines.isEmpty) {
val msg = runningEngines.map { case (x, y) => y }.mkString(" ")
concurrent.Future(Forbidden(Json.obj("message" -> msg)))
} else {
val timeout = play.api.libs.concurrent.Promise.timeout("Scheduler is unreachable. Giving up.", concurrent.duration.Duration(10, concurrent.duration.MINUTES))
val delete = Helper.deleteAppScheduler(app.id)
concurrent.Future.firstCompletedOf(Seq(delete, timeout)).map {
case r: SimpleResult => {
if (r.header.status == http.Status.OK) {
Helper.deleteApp(id, user.id, keepSettings = false)
}
r
}
case t: String => InternalServerError(Json.obj("message" -> t))
}
}
}
/**
* Erase appdata of this appid
*
* {{{
* POST
* JSON Parameters:
* None
* JSON Response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If erased successfully:
* Ok
* }}}
*
* @param id the App ID
*/
def eraseAppData(id: Int) = WithApp.async(id) { (user, app) =>
implicit request =>
val timeout = play.api.libs.concurrent.Promise.timeout("Scheduler is unreachable. Giving up.", concurrent.duration.Duration(10, concurrent.duration.MINUTES))
val delete = Helper.deleteAppScheduler(app.id)
concurrent.Future.firstCompletedOf(Seq(delete, timeout)).map {
case r: SimpleResult => {
if (r.header.status == http.Status.OK) {
Helper.deleteApp(id, user.id, keepSettings = true)
}
r
}
case t: String => InternalServerError(Json.obj("message" -> t))
}
}
/**
* Returns a list of available engine infos in the system
*
* {{{
* GET
* JSON Parameters:
* None
* JSON Response:
* Ok
* [ { "id" : <engine info id stirng>,
* "engineinfoname" : <name of the engine info>,
* "description": <description in html string>
* },
* ...
* ]
* }}}
*/
def getEngineInfoList = Action {
Ok(JsArray(engineInfos.getAll() map {
eng =>
Json.obj(
"id" -> eng.id,
"engineinfoname" -> eng.name,
"description" -> eng.description
)
}))
}
/**
* Returns a list of available algo infos of a specific engine info
*
* {{{
* GET
* JSON Parameters:
* None
* JSON Response:
* If the engine info id is not found:
* InternalServerError
* {
* "message" : "Invalid EngineInfo ID."
* }
*
* If found:
* Ok
* { "engineinfoname" : <the name of the engine info>,
* "algotypelist" : [ { "id" : <algo info id>,
* "algoinfoname" : <name of the algo info>,
* "description" : <string>,
* "req" : [<technology requirement string>],
* "datareq" : [<data requirement string>]
* }, ...
* ]
* }
* }}}
*
* @param id the engine info id
*/
def getEngineInfoAlgoInfoList(id: String) = Action {
engineInfos.get(id) map { engineInfo =>
Ok(Json.obj(
"engineinfoname" -> engineInfo.name,
"algotypelist" -> JsArray(
algoInfos.getByEngineInfoId(id).map { algoInfo =>
Json.obj(
"id" -> algoInfo.id,
"algoinfoname" -> algoInfo.name,
"description" -> algoInfo.description.getOrElse[String](""),
"req" -> Json.toJson(algoInfo.techreq),
"datareq" -> Json.toJson(algoInfo.datareq)
)
}
)
))
} getOrElse InternalServerError(Json.obj("message" -> s"Invalid EngineInfo ID: ${id}."))
}
/**
* Returns a list of available metric infos of a specific engine info
*
* {{{
* GET
* JSON Parameters:
* None
* JSON Response:
* If the engine info id is not found:
* InternalServerError
* {
* "message" : "Invalid EngineInfo ID."
* }
*
* If found:
* Ok
* { "engineinfoname" : <the name of the engine info>,
* "defaultmetric" : <default metric info id>,
* "metricslist" : [ { "id" : <metric info id>,
* "name" : <short name of the metric info>,
* "description" : <long name of the metric info>,
* }, ...
* ]
* }
* }}}
*
* @param id the engine info id
*/
def getEngineInfoMetricInfoList(id: String) = Action {
engineInfos.get(id) map { engInfo =>
val metrics = offlineEvalMetricInfos.getByEngineinfoid(engInfo.id).map { m =>
Json.obj(
"id" -> m.id,
"name" -> m.name,
"description" -> m.description
)
}
Ok(Json.obj(
"engineinfoname" -> engInfo.name,
"defaultmetric" -> engInfo.defaultofflineevalmetricinfoid,
"metricslist" -> JsArray(metrics)
))
} getOrElse {
InternalServerError(Json.obj("message" -> s"Invalid engineinfo ID: ${id}."))
}
}
/**
* Returns a list of available splitter infos of a specific engine info
*
* {{{
* GET
* JSON Parameters:
* None
* JSON Response:
* If the engine info id is not found:
* InternalServerError
* {
* "message" : "Invalid EngineInfo ID."
* }
*
* If found:
* Ok
* { "engineinfoname" : <the name of the engine info>,
* "defaultsplitter": <default splitter info id>,
* "splitterlist" : [ { "id" : <splitter info id>,
* "name" : <name of splitter>,
* "description" : <splitter description>,
* }, ...
* ]
* }
* }}}
*
* @param id the engine info id
*/
def getEngineInfoSplitterInfoList(id: String) = Action {
engineInfos.get(id).map { engInfo =>
val splitters = offlineEvalSplitterInfos.getByEngineinfoid(engInfo.id).map { m =>
Json.obj(
"id" -> m.id,
"name" -> m.name,
"description" -> m.description
)
}
Ok(Json.obj(
"engineinfoname" -> engInfo.name,
"defaultsplitter" -> engInfo.defaultofflineevalsplitterinfoid,
"splitterlist" -> JsArray(splitters)
))
}.getOrElse {
InternalServerError(Json.obj("message" -> s"Invalid engineinfo ID: ${id}."))
}
}
/**
* Returns list of engines of this appid
*
* {{{
* GET
* JSON parameters:
* None
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If no engine:
* NoContent
*
* If engines found:
* Ok
* {
* "id" : <appid int>,
* "enginelist" : [ { "id" : <engineid int>, "enginename" : <string>, "engineinfoid" : <string> },
* ....,
* { "id" : <engineid int>, "enginename" : <string>, "engineinfoid" : <string> } ]
*
* }
* }}}
*
* @param id the App ID
*/
def getAppEnginelist(appid: Int) = WithApp(appid) { (user, app) =>
implicit request =>
val appEngines = engines.getByAppid(appid)
if (!appEngines.hasNext) NoContent
else
Ok(Json.obj(
"id" -> appid,
"enginelist" -> JsArray(appEngines.map { eng =>
Json.obj("id" -> eng.id, "enginename" -> eng.name, "engineinfoid" -> eng.infoid)
}.toSeq)
))
}
/**
* Returns the engine of this engineid
*
* {{{
* GET
* JSON Parameters:
* None
* JSON Response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If engine not found:
* NotFound
* {
* "message" : "Invalid app id or engine id."
* }
*
* If engine found:
* Ok
* {
* "id" : <engine id>,
* "engineinfoid" : <engine info id>,
* "enginename" : <engine name>,
* "enginestatus" : <engine status>
* }
* }}}
* @note engine status:
* noappdata
* nodeployedalgo
* firsttraining
* nomodeldata
* nomodeldatanoscheduler
* training
* running
* runningnoscheduler
*
* @param appid the App ID
* @param id the engine ID
*/
def getEngine(appid: Int, id: Int) = WithEngine(appid, id) { (user, app, eng) =>
implicit request =>
val modelDataExist: Boolean = eng.infoid match {
case "itemrec" => try { itemRecScores.existByAlgo(algoOutputSelector.itemRecAlgoSelection(eng)) } catch { case e: RuntimeException => false }
case "itemsim" => try { itemSimScores.existByAlgo(algoOutputSelector.itemSimAlgoSelection(eng)) } catch { case e: RuntimeException => false }
case _ => false
}
val deployedAlgos = algos.getDeployedByEngineid(eng.id)
val hasDeployedAlgo = deployedAlgos.hasNext
val algo = if (deployedAlgos.hasNext) Some(deployedAlgos.next()) else None
val engineStatus: String =
if (appDataUsers.countByAppid(eng.appid) == 0 && appDataItems.countByAppid(eng.appid) == 0 && appDataU2IActions.countByAppid(eng.appid) == 0)
"noappdata"
else if (!hasDeployedAlgo)
"nodeployedalgo"
else if (!modelDataExist)
try {
(Await.result(WS.url(s"${settingsSchedulerUrl}/apps/${eng.appid}/engines/${eng.id}/algos/${algo.get.id}/status").get(), scala.concurrent.duration.Duration(5, SECONDS)).json \ "status").as[String] match {
case "jobrunning" => "firsttraining"
case _ => "nomodeldata"
}
} catch {
case e: java.net.ConnectException => "nomodeldatanoscheduler"
}
else
try {
(Await.result(WS.url(s"${settingsSchedulerUrl}/apps/${eng.appid}/engines/${eng.id}/algos/${algo.get.id}/status").get(), scala.concurrent.duration.Duration(5, SECONDS)).json \ "status").as[String] match {
case "jobrunning" => "training"
case _ => "running"
}
} catch {
case e: java.net.ConnectException => "runningnoscheduler"
}
Ok(Json.obj(
"id" -> eng.id, // engine id
"engineinfoid" -> eng.infoid,
"appid" -> eng.appid,
"enginename" -> eng.name,
"enginestatus" -> engineStatus))
}
/**
* Creates an Engine
*
* {{{
* POST
* JSON parameters:
* {
* "engineinfoid" : <engine info id>,
* "enginename" : <engine name>
* }
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* if invalid appid:
* NotFound
* {
* "message" : <error message>
* }
*
* If bad param:
* BadRequest
* {
* "message" : <error message>
* }
*
* If created:
* Ok
* {
* "id" : <new engine id>,
* "engineinfoid" : <engine info id>,
* "appid" : <app id>,
* "enginename" : <engine name>
* }
*
* }}}
*
* @param appid the App ID
*/
def createEngine(appid: Int) = WithApp(appid) { (user, app) =>
implicit request =>
val supportedEngineTypes: Seq[String] = engineInfos.getAll() map { _.id }
val enginenameConstraint = Constraints.pattern(nameRegex, "constraint.enginename", "Engine names should only contain alphanumerical characters, underscores, or dashes. The first character must be an alphabet.")
val engineForm = Form(tuple(
"engineinfoid" -> (text verifying ("This feature will be available soon.", e => supportedEngineTypes.contains(e))),
"enginename" -> (text verifying ("Please name your engine.", enginename => enginename.length > 0)
verifying enginenameConstraint)
) verifying ("Engine name must be unique.", f => !engines.existsByAppidAndName(appid, f._2))
verifying ("Engine type is invalid.", f => engineInfos.get(f._1).map(_ => true).getOrElse(false)))
engineForm.bindFromRequest.fold(
formWithError => {
val msg = formWithError.errors(0).message // extract 1st error message only
BadRequest(toJson(Map("message" -> toJson(msg))))
},
formData => {
val (enginetype, enginename) = formData
val engineInfo = engineInfos.get(enginetype).get
val engineId = engines.insert(Engine(
id = -1,
appid = appid,
name = enginename,
infoid = enginetype,
itypes = None, // NOTE: default None (means all itypes)
params = engineInfo.params.map(s => (s._2.id, s._2.defaultvalue))
))
Logger.info("Create engine ID " + engineId)
// automatically create default algo
val defaultAlgoType = engineInfo.defaultalgoinfoid
val params = algoInfos.get(defaultAlgoType).get.params.mapValues(_.defaultvalue)
val defaultAlgo = Algo(
id = -1,
engineid = engineId,
name = "Default-Algo",
infoid = defaultAlgoType,
command = "",
params = params,
settings = Map(), // no use for now
modelset = false, // init value
createtime = DateTime.now,
updatetime = DateTime.now,
status = "deployed", // this is default deployed algo
offlineevalid = None,
loop = None
)
val algoId = algos.insert(defaultAlgo)
Logger.info("Create algo ID " + algoId)
WS.url(settingsSchedulerUrl + "/users/" + user.id + "/sync").get()
Ok(Json.obj(
"id" -> engineId, // engine id
"engineinfoid" -> enginetype,
"appid" -> appid,
"enginename" -> enginename))
}
)
}
/**
* Removes an engine
*
* {{{
* DELETE
* JSON parameters:
* None
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If deleted:
* Ok
*
* }}}
*
* @param appid the App ID
* @param engineid the engine ID
*/
def removeEngine(appid: Int, engineid: Int) = WithEngine.async(appid, engineid) { (user, app, eng) =>
implicit request =>
// don't delete if there is any sim eval and offline tune pending, or deployed algorithm
val pendingSimEvals = Helper.getSimEvalsByEngineid(eng.id).filter(x => Helper.isPendingSimEval(x)).toList
val pendingOfflineTunes = offlineTunes.getByEngineid(eng.id).filter(x => Helper.isPendingOfflineTune(x)).toList
val deployedAlgos = algos.getDeployedByEngineid(eng.id).toList
if (deployedAlgos.size != 0) {
val names = deployedAlgos map (x => x.name) mkString (",")
Future.successful(Forbidden(Json.obj("message" -> s"This engine has deployed algorithms (${names}). Please undeploy them before delete this engine.")))
} else if (pendingSimEvals.size != 0) {
Future.successful(Forbidden(Json.obj("message" -> "There are running simulated evaluations. Please stop and delete them before delete this engine.")))
} else if (pendingOfflineTunes.size != 0) {
Future.successful(Forbidden(Json.obj("message" -> "There are auto-tuning algorithms. Please stop and delete them before delete this engine.")))
} else {
/** Deletion could take a while */
val timeout = play.api.libs.concurrent.Promise.timeout("Scheduler is unreachable. Giving up.", concurrent.duration.Duration(10, concurrent.duration.MINUTES))
// to scheduler: delete engine
val delete = Helper.deleteEngineScheduler(appid, engineid)
concurrent.Future.firstCompletedOf(Seq(delete, timeout)).map {
case r: SimpleResult => {
if (r.header.status == http.Status.OK) {
Helper.deleteEngine(engineid, appid, keepSettings = false)
}
r
}
case t: String => InternalServerError(Json.obj("message" -> t))
}
}
}
/**
* Returns a list of available (added but not deployed) algorithms of this engine
* {{{
* GET
* JSON parameters:
* None
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If no algo:
* NoContent
*
* If algos found:
* [ { see algoToJson
* }, ...
* ]
* }}}
* @note algo status
* TODO: add more info here...
*
* @param appid the App ID
* @param engineid the engine ID
*/
def getAvailableAlgoList(appid: Int, engineid: Int) = WithEngine(appid, engineid) { (user, app, eng) =>
implicit request =>
val engineAlgos = algos.getByEngineid(engineid).filter { Helper.isAvailableAlgo(_) }
if (!engineAlgos.hasNext) NoContent
else
Ok(Json.toJson( // NOTE: only display algos which are not "deployed", nor "simeval"
engineAlgos.map { algo =>
val algoInfo = algoInfos.get(algo.infoid)
algoToJson(algo, appid, algoInfo)
}.toSeq
))
}
/**
* Returns an available algorithm
* {{{
* GET
* JSON parameters:
* None
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If algo not found:
* NotFound
* {
* "message" : "Invalid app id, engine id or algo id."
* }
*
* If found:
* Ok
* {
* see algoToJson
* }
* }}}
*
* @param appid the App ID
* @param engineid the engine ID
* @param id the algo ID
*/
def getAvailableAlgo(appid: Int, engineid: Int, id: Int) = WithAlgo(appid, engineid, id) { (user, app, eng, algo) =>
implicit request =>
algoInfos.get(algo.infoid).map { info =>
Ok(algoToJson(algo, appid, Some(info)))
}.getOrElse {
InternalServerError(Json.obj("message" -> s"Algoinfo ${algo.infoid} not found."))
}
}
/**
* Creates an new algorithm
* {{{
* POST
* JSON parameters:
* {
* "algoinfoid" : <algo info id>,
* "algoname" : <algo name>,
* }
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If invalid appid or engineid:
* NotFound
* {
* "message" : <error message>
* }
*
* If creation failed:
* BadRequest
* {
* "message" : <error message>
* }
*
* If created:
* Ok
* {
* see algoToJson
* }
*
* }}}
*
* @param appid the App ID
* @param engineid the engine ID
*/
def createAvailableAlgo(appid: Int, engineid: Int) = WithEngine(appid, engineid) { (user, app, eng) =>
implicit request =>
val supportedAlgoTypes: Seq[String] = algoInfos.getAll map { _.id }
val algonameConstraint = Constraints.pattern(nameRegex, "constraint.algoname", "Algorithm names should only contain alphanumerical characters, underscores, or dashes. The first character must be an alphabet.")
val createAlgoForm = Form(tuple(
"algoinfoid" -> (nonEmptyText verifying ("This feature will be available soon.", t => supportedAlgoTypes.contains(t))),
"algoname" -> (text verifying ("Please name your algo.", name => name.length > 0)
verifying algonameConstraint) // same name constraint as engine
) verifying ("Algo name must be unique.", f => !algos.existsByEngineidAndName(engineid, f._2)))
createAlgoForm.bindFromRequest.fold(
formWithError => {
val msg = formWithError.errors(0).message // extract 1st error message only
BadRequest(toJson(Map("message" -> toJson(msg))))
},
formData => {
val (algoType, algoName) = formData
algoInfos.get(algoType).map { algoInfo =>
val newAlgo = Algo(
id = -1,
engineid = engineid,
name = algoName,
infoid = algoType,
command = "",
params = algoInfo.params.mapValues(_.defaultvalue),
settings = Map(), // no use for now
modelset = false, // init value
createtime = DateTime.now,
updatetime = DateTime.now,
status = "ready", // default status
offlineevalid = None,
loop = None
)
val algoId = algos.insert(newAlgo)
Logger.info("Create algo ID " + algoId)
Ok(algoToJson(newAlgo.copy(id = algoId), appid, Some(algoInfo)))
}.getOrElse {
InternalServerError(Json.obj("message" -> s"Algoinfo ${algoType} not found."))
}
}
)
}
/**
* Deletes an algorithm
* {{{
* DELETE
* JSON parameters:
* None
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If deleted:
* Ok
* }}}
*
* @param appid the App ID
* @param engineid the engine ID
* @param id the algo ID
*/
def removeAvailableAlgo(appid: Int, engineid: Int, id: Int) = WithAlgo.async(appid, engineid, id) { (user, app, eng, algo) =>
implicit request =>
/** Deletion could take a while */
val timeout = play.api.libs.concurrent.Promise.timeout("Scheduler is unreachable. Giving up.", concurrent.duration.Duration(10, concurrent.duration.MINUTES))
val deleteTune = algo.offlinetuneid map { tuneid =>
offlineTunes.get(tuneid) map { tune =>
/** Make sure to unset offline tune's creation time to prevent scheduler from picking up */
offlineTunes.update(tune.copy(createtime = None))
Helper.stopAndDeleteOfflineTuneScheduler(appid, engineid, tuneid)
} getOrElse {
concurrent.Future { Ok }
}
} getOrElse {
concurrent.Future { Ok }
}
val deleteAlgo: concurrent.Future[SimpleResult] = Helper.deleteAlgoScheduler(appid, engineid, algo.id)
val complete: concurrent.Future[SimpleResult] = concurrent.Future.reduce(Seq(deleteTune, deleteAlgo)) { (a, b) =>
if (a.header.status != http.Status.OK) // keep the 1st error
a
else
b
}
concurrent.Future.firstCompletedOf(Seq(complete, timeout)).map {
case r: SimpleResult => {
if (r.header.status == http.Status.OK) {
algo.offlinetuneid map { tuneid =>
Helper.deleteOfflineTune(tuneid, keepSettings = false)
}
Helper.deleteAlgo(algo.id, keepSettings = false)
}
r
}
case t: String => InternalServerError(Json.obj("message" -> t))
}
}
/**
* Returns a list of deployed algorithms
* {{{
* GET
* JSON parameters:
* None
* JSON repseon:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If no deployed algo:
* NoContent
*
* If deployed algos found:
* {
* "updatedtime" : <TODO>,
* "status" : <TODO>,
* "algolist" : [ { see algoToJson
* }, ...
* ]
* }
* }}}
*
* @param appid the App ID
* @param engineid the engine ID
*/
def getDeployedAlgoList(appid: Int, engineid: Int) = WithEngine(appid, engineid) { (user, app, eng) =>
implicit request =>
val deployedAlgos = algos.getDeployedByEngineid(engineid)
if (!deployedAlgos.hasNext) NoContent
else
Ok(Json.obj(
"updatedtime" -> "12-03-2012 12:32:12", // TODO: what's this time for?
"status" -> "Running",
"algolist" -> Json.toJson(deployedAlgos.map { algo =>
val algoInfo = algoInfos.get(algo.infoid)
algoToJson(algo, appid, algoInfo)
}.toSeq)
))
}
/**
* Deploys a list of algorithms (also undeploys any existing deployed algorithms)
* The status of deployed algorithms change to "deployed"
*
* {{{
* POST
* JSON parameters:
* {
* "algoidlist" : [ array of algo ids ]
* }
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If any of the algo id is not valid:
* BadRequest
* {
* "message" : <error message>
* }
*
* If done:
* Ok
*
* }}}
*
* @param appid the App ID
* @param engineid the engine ID
*/
def algoDeploy(appid: Int, engineid: Int) = WithEngine(appid, engineid) { (user, app, eng) =>
implicit request =>
val deployForm = Form(
"algoidlist" -> list(number)
)
deployForm.bindFromRequest.fold(
formWithErrors => Ok,
form => {
val algoidList = form
val algoList = algoidList.map(id => (id, algos.getByIdAndEngineid(id, engineid)))
val invalidAlgos = algoList.filter {
case (id, algoOpt) => algoOpt match {
case None => true // not exist
case Some(x) => (x.status != "ready") // not ready
}
}
// make sure all algoids are valid
if (!invalidAlgos.isEmpty) {
val ids = invalidAlgos.map { case (id, algoOpt) => id }.mkString(", ")
BadRequest(Json.obj("message" -> s"Invalid algo ids: ${ids}."))
} else {
algos.getDeployedByEngineid(engineid).foreach { algo =>
algos.update(algo.copy(status = "ready"))
}
algoList.foreach {
case (id, algoOpt) =>
// algoOpt can't be None because of invalidAlgos check
algos.update(algoOpt.get.copy(status = "deployed"))
}
WS.url(settingsSchedulerUrl + "/users/" + user.id + "/sync").get()
Ok
}
}
)
}
/**
* Undeploys all deployed algorithms
* {{{
* POST
* JSON parameters:
* None
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If done:
* Ok
* }}}
*
* @param appid the App ID
* @param engineid the engine ID
*/
def algoUndeploy(appid: Int, engineid: Int) = WithEngine(appid, engineid) { (user, app, eng) =>
implicit request =>
algos.getDeployedByEngineid(engineid) foreach { algo =>
algos.update(algo.copy(status = "ready"))
}
WS.url(settingsSchedulerUrl + "/users/" + user.id + "/sync").get()
Ok
}
/**
* Requests to train model now
* {{{
* POST
* JSON parameters:
* None
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If error:
* InternalServerError
* {
* "message" : <error message>
* }
*
* If done:
* Ok
* {
* "message" : <message from scheduler>
* }
* }}}
*
* @param appid the App ID
* @param engineid the engine ID
*/
def algoTrainNow(appid: Int, engineid: Int) = WithEngine.async(appid, engineid) { (user, app, eng) =>
implicit request =>
// No extra param required
val timeout = play.api.libs.concurrent.Promise.timeout("Scheduler is unreachable. Giving up.", concurrent.duration.Duration(10, concurrent.duration.MINUTES))
val request = WS.url(s"${settingsSchedulerUrl}/apps/${appid}/engines/${engineid}/trainoncenow").get() map { r =>
Ok(Json.obj("message" -> (r.json \ "message").as[String]))
} recover {
case e: Exception => InternalServerError(Json.obj("message" -> e.getMessage()))
}
/** Detect timeout (10 minutes by default) */
concurrent.Future.firstCompletedOf(Seq(request, timeout)).map {
case r: SimpleResult => r
case t: String => InternalServerError(Json.obj("message" -> t))
}
}
/**
* Returns a list of simulated evalulation for this engine
*
* {{{
* GET
* JSON parameters:
* None
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If no sim evals:
* NoContent
*
* If found:
* Ok
* [
* { "id" : <sim eval id>,
* "appid" : <app id>,
* "engineid" : <engine id>,
* "algolist" : [
* { "id" : <algo id>,
* "algoname" : <algo name>,
* "appid" : <app id>,
* "engineid" : <engine id>,
* "algoinfoid" : <algo info id>,
* "algoinfoname" : <algo info name>,
* "settingsstring" : <algo setting string>
* }, ...
* ],
* "status" : <sim eval status>,
* "createtime" : <sim eval create time>,
* "endtime" : <sim eval end time>
* }, ...
* ]
* }}}
*
* @param appid the App ID
* @param engineid the engine ID
*/
def getSimEvalList(appid: Int, engineid: Int) = WithEngine(appid, engineid) { (user, app, eng) =>
implicit request =>
// get offlineeval for this engine
val engineOfflineEvals = Helper.getSimEvalsByEngineid(engineid)
if (!engineOfflineEvals.hasNext) NoContent
else {
val resp = Json.toJson(
engineOfflineEvals.map { eval =>
val algolist = Json.toJson(
algos.getByOfflineEvalid(eval.id).map { algo =>
val algoInfo = algoInfos.get(algo.infoid)
algoToJson(algo, appid, algoInfo, withParam = true)
}.toSeq
)
val createtime = eval.createtime.map(dateTimeToString(_)).getOrElse("-")
val starttime = eval.starttime.map(dateTimeToString(_)).getOrElse("-")
val endtime = eval.endtime.map(dateTimeToString(_)).getOrElse("-")
Json.obj(
"id" -> eval.id,
"appid" -> appid,
"engineid" -> eval.engineid,
"algolist" -> algolist,
"status" -> getSimEvalStatus(eval),
"createtime" -> createtime, // NOTE: use createtime here for test date
"endtime" -> endtime
)
}.toSeq
)
Ok(resp)
}
}
/**
* Creates a simulated evalution
*
* {{{
* POST
* JSON parameters:
*
* {
* "infoid[i]": <metric or splitter info id>
* "infotype[i]": <"offlineevalmetric" or "offlineevalsplitter">
* "other param [i]": <the parameter value for corresponding infoid[i]>
* "algo" : <list of algo ids to be evaluated>
* "splittrain": <training set split percentage 1 to 100>,
* "splittest": <test set split percentage 1 to 100>,
* "evaliteration": <number of iterations>
* }
*
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If error:
* BadRequest
* {
* "message" : <error message>
* }
*
* If created:
* Ok
*
* }}}
* @param appid the App ID
* @param engineid the engine ID
*/
def createSimEval(appid: Int, engineid: Int) = WithEngine(appid, engineid) { (user, app, eng) =>
implicit request =>
val simEvalForm = Form(tuple(
"infoid" -> seqOfMapOfStringToAny,
"algoids" -> list(number), // algo id
"splittrain" -> number(1, 100),
"splittest" -> number(1, 100),
"evaliteration" -> (number verifying ("Number of Iteration must be greater than 0", x => (x > 0)))
))
simEvalForm.bindFromRequest.fold(
e => BadRequest(Json.obj("message" -> e.toString)),
f => {
val (params, algoids, splitTrain, splitTest, evalIteration) = f
val metricList = params.filter(p => (p("infotype") == "offlineevalmetric")).map(p =>
OfflineEvalMetric(
id = 0,
infoid = p("infoid").asInstanceOf[String],
evalid = 0, // will be assigned later
params = p - "infoid" - "infotype" // remove these keys from params
)).toList
// percentage param is standard for all splitter
val percentageParam = Map(
"trainingPercent" -> (splitTrain.toDouble / 100),
"validationPercent" -> 0.0, // no validatoin set for sim eval
"testPercent" -> (splitTest.toDouble / 100)
)
val splitterList = params.filter(p => (p("infotype") == "offlineevalsplitter")).map(p =>
OfflineEvalSplitter(
id = 0,
evalid = 0, // will be assigned later
name = "", // will be assigned later
infoid = p("infoid").asInstanceOf[String],
settings = p ++ percentageParam - "infoid" - "infotype" // remove these keys from params
)).toList
// get list of algo obj
val optAlgos: List[Option[Algo]] = algoids.map { algos.get(_) }
if (metricList.length == 0)
BadRequest(Json.obj("message" -> "At least one metric is required."))
else if (splitterList.length == 0)
BadRequest(Json.obj("message" -> "One Splitter is required."))
else if (splitterList.length > 1)
BadRequest(Json.obj("message" -> "Multiple Splitters are not supported now."))
else if (optAlgos.contains(None))
BadRequest(Json.obj("message" -> "Invalid algo ids."))
else {
val algoList: List[Algo] = optAlgos.map(_.get) // NOTE: already check optAlgos does not contain None
val evalid = createOfflineEval(eng, algoList, metricList, splitterList(0), evalIteration)
WS.url(settingsSchedulerUrl + "/users/" + user.id + "/sync").get()
Ok
}
}
)
}
/**
* Requests to stop and delete simulated evalution (including running/pending job)
*
* {{{
* DELETE
* JSON parameters:
* None
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If not found:
* NotFound
* {
* "message" : <error message>
* }
*
* If error:
* InternalServerError
* {
* "message" : <error message>
* }
*
* If deleted:
* Ok
* {
* "message" : "Offline evaluation ID $id has been deleted"
* }
* }}}
*
* @param appid the App ID
* @param engineid the engine ID
* @param id the offline evaluation ID
*
*/
def removeSimEval(appid: Int, engineid: Int, id: Int) = WithOfflineEval.async(appid, engineid, id) { (user, app, eng, oe) =>
implicit request =>
// remove algo, remove metric, remove offline eval
/** Make sure to unset offline eval's creation time to prevent scheduler from picking up */
offlineEvals.update(oe.copy(createtime = None))
/** Deletion of app data and model data could take a while */
val timeout = play.api.libs.concurrent.Promise.timeout("Scheduler is unreachable. Giving up.", concurrent.duration.Duration(10, concurrent.duration.MINUTES))
val complete = Helper.stopAndDeleteSimEvalScheduler(appid, engineid, oe.id)
/** Detect timeout (10 minutes by default) */
concurrent.Future.firstCompletedOf(Seq(complete, timeout)).map {
case r: SimpleResult => {
if (r.header.status == http.Status.OK) {
Helper.deleteOfflineEval(oe.id, keepSettings = false)
}
r
}
case t: String => InternalServerError(Json.obj("message" -> t))
}
}
/**
* Returns the simulated evaluation report
*
* {{{
* GET
* JSON parameters:
* None
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If not found:
* {
* "message" : <error message>
* }
*
* If found:
* Ok
* {
* "id" : <sim eval id>,
* "appid" : <app id>,
* "engineid" : <engine id>,
* "algolist" : [
* {
* "id" : <algo id>,
* "algoname" : <algo name>,
* "appid" : <app id>,
* "engineid" : <engine id>,
* "algoinfoid" : <algo info id>,
* "algoinfoname" : <algo info name>,
* "settingsstring" : <algo setting string>
* }, ...
* ],
* "metricslist" : [
* {
* "id" : <metric id>,
* "metricsinfoid" : <metric info id>,
* "metricsname" : <metric info name>,
* "settingsstring" : <metric setting string>
* }, ...
* ],
* "metricscorelist" : [
* {
* "algoid" : <algo id>,
* "metricsid" : <metric id>,
* "score" : <average score>
* }, ...
* ],
* "metricscoreiterationlist" : [
* [{
* "algoid" : <algo id>,
* "metricsid" : <metric id>,
* "score" : <score of 1st iteration>
* }, ...
* ],
* [{
* "algoid" : <algo id>,
* "metricsid" : <metric id>,
* "score" : <score of 2nd iteration>
* }, ...
* ],
* ],
* "splittrain" : <training set split percentage 1 to 100>,
* "splittest" : <test set split percentage 1 to 100>,
* "splittersettingsstring" : <splitter setting string>,
* "evaliteration" : <number of iterations>,
* "status" : <eval status>,
* "starttime" : <start time>,
* "endtime" : <end time>
* }
* }}}
*
* @param appid the App ID
* @param engineid the engine ID
* @param id the offline evaluation ID
*/
def getSimEvalReport(appid: Int, engineid: Int, id: Int) = WithOfflineEval(appid, engineid, id) { (user, app, eng, eval) =>
implicit request =>
val status = getSimEvalStatus(eval)
val evalAlgos = algos.getByOfflineEvalid(eval.id).toArray
val algolist = Json.toJson(
evalAlgos.map { algo =>
val algoInfo = algoInfos.get(algo.infoid)
algoToJson(algo, appid, algoInfo, withParam = true)
}.toSeq
)
val evalMetrics = offlineEvalMetrics.getByEvalid(eval.id).toArray
val metricslist = Json.toJson(
evalMetrics.map { metric =>
val metricInfo = offlineEvalMetricInfos.get(metric.infoid)
offlineEvalMetricToJson(metric, metricInfo, withParam = true)
}.toSeq
)
val evalResults = offlineEvalResults.getByEvalid(eval.id).toArray
val metricscorelist = Json.toJson(
(for (algo <- evalAlgos; metric <- evalMetrics) yield {
val results = evalResults
.filter(x => ((x.metricid == metric.id) && (x.algoid == algo.id)))
.map(x => x.score)
val num = results.length
val avg = if ((results.isEmpty) || (num != eval.iterations)) "N/A" else ((results.reduceLeft(_ + _) / num).toString)
Json.obj(
"algoid" -> algo.id,
"metricsid" -> metric.id,
"score" -> avg
)
}).toSeq
)
val metricscoreiterationlist = Json.toJson((for (i <- 1 to eval.iterations) yield {
val defaultScore = (for (algo <- evalAlgos; metric <- evalMetrics) yield {
((algo.id, metric.id) -> "N/A") // default score
}).toMap
val evalScore = evalResults.filter(x => (x.iteration == i)).map(r =>
((r.algoid, r.metricid) -> r.score.toString)).toMap
// overwrite defaultScore with evalScore
(defaultScore ++ evalScore).map {
case ((algoid, metricid), score) =>
Json.obj("algoid" -> algoid, "metricsid" -> metricid, "score" -> score)
}
}).toSeq)
val starttime = eval.starttime map (dateTimeToString(_)) getOrElse ("-")
val endtime = eval.endtime map (dateTimeToString(_)) getOrElse ("-")
// get splitter data
val splitters = offlineEvalSplitters.getByEvalid(eval.id)
if (splitters.hasNext) {
val splitter = splitters.next
val splitTrain = ((splitter.settings("trainingPercent").asInstanceOf[Double]) * 100).toInt
val splitTest = ((splitter.settings("testPercent").asInstanceOf[Double]) * 100).toInt
if (splitters.hasNext) {
InternalServerError(Json.obj("message" -> s"More than one splitter found for this Offline Eval ID: ${eval.id}"))
} else {
Ok(Json.obj(
"id" -> eval.id,
"appid" -> appid,
"engineid" -> eval.engineid,
"algolist" -> algolist,
"metricslist" -> metricslist,
"metricscorelist" -> metricscorelist,
"metricscoreiterationlist" -> metricscoreiterationlist,
"splittrain" -> splitTrain,
"splittest" -> splitTest,
"splittersettingsstring" -> offlineEvalSplitterParamToString(splitter, offlineEvalSplitterInfos.get(splitter.infoid)),
"evaliteration" -> eval.iterations,
"status" -> status,
"starttime" -> starttime,
"endtime" -> endtime
))
}
} else {
InternalServerError(Json.obj("message" -> s"No splitter found fo this Offline Eval ID ${eval.id}."))
}
}
/**
* Returns the algorithm auto tune report
*
* {{{
* GET
* JSON parameters:
* None
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If not found:
* NotFound
* {
* "message" : <error message>
* }
*
* If found:
* Ok
* {
* "id" : <original algo id>,
* "appid" : <app id>,
* "engineid" : <engine id>,
* "algo" : {
* "id" : <original algo id>,
* "algoname" : <algo name>,
* "appid" : <app id>,
* "engineid" : <engine id>,
* "algoinfoid" : <algo info id>,
* "algoinfoname" : <algo info name>,
* "settingsstring" : <algo setting string>
* },
* "metric" : {
* "id" : <metric id>,
* "metricsinfoid" : <metric info id>,
* "metricsname" : <metric info name>,
* "settingsstring" : <metric setting string>
* },
* "metricscorelist" : [
* {
* "algoautotuneid" : <tuned algo id>,
* "algoinfoname" : <algo info name>,
* "settingsstring" : <algo setting string>,
* "score" : <average score>
* }, ...
* ],
* "metricscoreiterationlist" : [
* [ {
* "algoautotuneid" : <tuned algo id 1>,
* "algoinfoname" : <algo info name>,
* "settingsstring" : <algo setting string>,
* "score" : <score of 1st iteration for this tuned algo id>
* },
* {
* "algoautotuneid" : <tuned algo id 2>,
* "algoinfoname" : <algo info name>,
* "settingsstring" : <algo setting string>,
* "score" : <score of 1st iteration for this tuned algo id>
* }, ...
* ],
* [ {
* "algoautotuneid" : <tuned algo id 1>,
* "algoinfoname" : <algo info name>,
* "settingsstring" : <algo setting string>,
* "score" : <score of 2nd iteration for this tuned algo id>
* },
* {
* "algoautotuneid" : <tuned algo id 2>,
* "algoinfoname" : <algo info name>,
* "settingsstring" : <algo setting string>,
* "score" : <score of 2nd iteration for this tuned algo id>
* }, ...
* ], ...
* ],
* "splittrain" : <training set split percentage 1 to 100>,
* "splitvalidation" : <validation set split percentage 1 to 100>,
* "splittest" : <test set split percentage 1 to 100>,
* "splitmethod" : <split method string: randome, time>
* "splittersettingsstring" : <splitter setting string>,
* "evaliteration" : <number of iterations>,
* "status" : <auto tune status>,
* "starttime" : <start time>,
* "endtime" : <end time>
* }
* }}}
*
* @param appid the App ID
* @param engineid the engine ID
* @param algoid the algo ID
*/
def getAlgoAutotuningReport(appid: Int, engineid: Int, algoid: Int) = WithAlgo(appid, engineid, algoid) { (user, app, eng, algo) =>
implicit request =>
algo.offlinetuneid map { tuneid =>
offlineTunes.get(tuneid) map { tune =>
val algoInfo = algoInfos.get(algo.infoid)
// get all offlineeval of this offlinetuneid
val tuneOfflineEvals: Array[OfflineEval] = offlineEvals.getByTuneid(tuneid).toArray.sortBy(_.id)
val tuneMetrics: Array[OfflineEvalMetric] = tuneOfflineEvals.flatMap { e => offlineEvalMetrics.getByEvalid(e.id) }
val tuneSplitters: Array[OfflineEvalSplitter] = tuneOfflineEvals.flatMap { e => offlineEvalSplitters.getByEvalid(e.id) }
// get all offlineeavlresults of each offlineevalid
val tuneOfflineEvalResults: Array[OfflineEvalResult] = tuneOfflineEvals.flatMap { e => offlineEvalResults.getByEvalid(e.id) }
// test set score
val tuneOfflineEvalResultsTestSet: Array[OfflineEvalResult] = tuneOfflineEvalResults.filter(x => (x.splitset == "test"))
val tuneOfflineEvalResultsTestSetAlgoidMap: Map[Int, Double] = tuneOfflineEvalResultsTestSet.map(x => (x.algoid -> x.score)).toMap
// get all algos of each offlineevalid
// note: this happen after getting all available offlineEvalResults,
// so these retrieved algos may have more algo than those used in offlineEvalResults.
val tuneAlgos: Array[Algo] = tuneOfflineEvals flatMap { e => algos.getByOfflineEvalid(e.id) }
val tuneAlgosMap: Map[Int, Algo] = tuneAlgos.map { a => (a.id -> a) }.toMap
// group by (loop, paramset)
type AlgoGroupIndex = (Option[Int], Option[Int])
val tuneAlgosGroup: Map[AlgoGroupIndex, Array[Algo]] = tuneAlgos.groupBy(a => (a.loop, a.paramset))
// get param of each group
val tuneAlgosGroupParams: Map[AlgoGroupIndex, (Int, String, String)] = tuneAlgosGroup.map {
case (index, arrayOfAlgos) =>
val tAlgo = arrayOfAlgos(0) // just take 1, all algos of this group will have same params
val tAlgoInfo: Option[AlgoInfo] = algoInfos.get(tAlgo.infoid)
val infoName = tAlgoInfo.map(_.name).getOrElse("Algo info ${tAlgo.infoid} not found")
val settings = algoParamToString(tAlgo, tAlgoInfo)
(index -> (tAlgo.id, settings, infoName))
}
// calculate avg
val avgScores: Map[AlgoGroupIndex, String] = tuneAlgosGroup.mapValues { arrayOfAlgos =>
// check if all scores available
val allAvailable = arrayOfAlgos.map(algo => tuneOfflineEvalResultsTestSetAlgoidMap.contains(algo.id)).reduceLeft(_ && _)
if (allAvailable) {
val scores: Array[Double] = arrayOfAlgos.map(algo => tuneOfflineEvalResultsTestSetAlgoidMap(algo.id))
(scores.sum / scores.size).toString
} else {
"N/A"
}
}
val metricscorelist = avgScores.toSeq.sortBy(_._1).map {
case (k, v) =>
Json.obj(
"algoautotuneid" -> tuneAlgosGroupParams(k)._1,
"algoinfoname" -> tuneAlgosGroupParams(k)._3,
"settingsstring" -> tuneAlgosGroupParams(k)._2,
"score" -> v)
}
val metricscoreiterationlist = tuneOfflineEvals.map { e =>
tuneOfflineEvalResultsTestSet.filter(x => (x.evalid == e.id)).sortBy(_.algoid).map { r =>
val tAlgo = tuneAlgosMap(r.algoid)
val tAlgoInfo: Option[AlgoInfo] = algoInfos.get(tAlgo.infoid)
val infoName = tAlgoInfo.map(_.name).getOrElse("Algo info ${tAlgo.infoid} not found")
Json.obj(
"algoautotuneid" -> r.algoid,
"algoinfoname" -> infoName,
"settingsstring" -> algoParamToString(tAlgo, tAlgoInfo),
"score" -> r.score)
}.toSeq
}.toSeq
val metric = tuneMetrics(0)
val metricInfo = offlineEvalMetricInfos.get(metric.infoid)
val splitter = tuneSplitters(0)
val splitTrain = ((splitter.settings("trainingPercent").asInstanceOf[Double]) * 100).toInt
val splitValidation = ((splitter.settings("validationPercent").asInstanceOf[Double]) * 100).toInt
val splitTest = ((splitter.settings("testPercent").asInstanceOf[Double]) * 100).toInt
val evalIteration = tuneOfflineEvals.size // NOTE: for autotune, number of offline eval is the iteration
val engineinfoid = engines.get(engineid) map { _.infoid } getOrElse { "unkown-engine" }
val starttime = tune.starttime map (dateTimeToString(_)) getOrElse ("-")
val endtime = tune.endtime map (dateTimeToString(_)) getOrElse ("-")
Ok(Json.obj(
"id" -> algoid,
"appid" -> appid,
"engineid" -> engineid,
"algo" -> algoToJson(algo, appid, algoInfo, withParam = true),
"metric" -> offlineEvalMetricToJson(metric, metricInfo, withParam = true),
"metricscorelist" -> Json.toJson(metricscorelist),
"metricscoreiterationlist" -> Json.toJson(metricscoreiterationlist),
"splittrain" -> splitTrain,
"splitvalidation" -> splitValidation,
"splittest" -> splitTest,
"splittersettingsstring" -> offlineEvalSplitterParamToString(splitter, offlineEvalSplitterInfos.get(splitter.infoid)),
"evaliteration" -> evalIteration,
"status" -> getOfflineTuneStatus(tune),
"starttime" -> starttime,
"endtime" -> endtime
))
} getOrElse {
InternalServerError(Json.obj("message" -> s"The offline tune id ${tuneid} does not exist."))
}
} getOrElse {
InternalServerError(Json.obj("message" -> "This algo does not have offline tune."))
}
}
/**
* Applies the selected tuned algo's params to this algo
*
* {{{
* POST
* JSON parameters:
* {
* "tunedalgoid" : <the tuned algo id. This algo's parameters will be used>
* }
* JSON response:
* If not authenticated:
* Forbidden
* {
* "message" : "Haven't signed in yet."
* }
*
* If error:
* BadRequest
* {
* "message" : <error message>
* }
*
* If algo not found:
* NotFound
* {
* "message" : <error message>
* }
* If applied:
* Ok
*
* }}}
*
* @param appid the App ID
* @param engineid the engine ID
* @param algoid the algo ID to which the tuned parameters are applied
*/
def algoAutotuningSelect(appid: Int, engineid: Int, algoid: Int) = WithAlgo(appid, engineid, algoid) { (user, app, eng, algo) =>
implicit request =>
// Apply POST request params of tunedalgoid to algoid
// update the status of algoid from 'tuning' or 'tuned' to 'ready'
val form = Form("tunedalgoid" -> number)
form.bindFromRequest.fold(
formWithError => {
val msg = formWithError.errors(0).message // extract 1st error message only
BadRequest(toJson(Map("message" -> toJson(msg))))
},
formData => {
val tunedAlgoid = formData
val orgAlgo = algos.get(algoid)
val tunedAlgo = algos.get(tunedAlgoid)
if ((orgAlgo == None) || (tunedAlgo == None)) {
NotFound(toJson(Map("message" -> toJson("Invalid app id, engine id or algo id."))))
} else {
val tunedAlgoParams = tunedAlgo.get.params ++ Map("tune" -> "manual")
algos.update(orgAlgo.get.copy(
params = tunedAlgoParams,
status = "ready"
))
Ok
}
}
)
}
def getEngineSettings(appid: Int, engineid: Int) = WithEngine(appid, engineid) { (user, app, engine) =>
implicit request =>
engineInfos.get(engine.infoid) map { engineInfo =>
val params = engineInfo.params.mapValues(_.defaultvalue) ++ engine.params
Ok(toJson(Map(
"id" -> toJson(engine.id), // engine id
"appid" -> toJson(engine.appid),
"allitemtypes" -> toJson(engine.itypes == None),
"itemtypelist" -> engine.itypes.map(x => toJson(x.toIterator.toSeq)).getOrElse(JsNull)) ++
(params map { case (k, v) => (k, toJson(v.toString)) })))
} getOrElse {
NotFound(toJson(Map("message" -> toJson(s"Invalid EngineInfo ID: ${engine.infoid}"))))
}
}
def updateEngineSettings(appid: Int, engineid: Int) = WithEngine(appid, engineid) { (user, app, engine) =>
implicit request =>
val f = Form(tuple(
"infoid" -> mapOfStringToAny,
"allitemtypes" -> boolean,
"itemtypelist" -> list(text)))
f.bindFromRequest.fold(
e => BadRequest(toJson(Map("message" -> toJson(e.toString)))),
f => {
val (params, allitemtypes, itemtypelist) = f
// NOTE: read-modify-write the original param
val itypes = if (itemtypelist.isEmpty) None else Option(itemtypelist)
val updatedParams = engine.params ++ params - "infoid"
val updatedEngine = engine.copy(itypes = itypes, params = updatedParams)
engines.update(updatedEngine)
Ok
})
}
def getEngineTemplateHtml(appid: Int, engineid: Int) = WithEngine(appid, engineid) { (user, app, engine) =>
implicit request =>
//Ok(views.html.engines.template(engine.infoid))
engineInfos.get(engine.infoid) map { engineInfo =>
if (engineInfo.paramsections.isEmpty)
Ok(views.html.engines.template(handleParamSections[EngineInfo](Seq(ParamSection(
name = "Parameter Settings",
description = Some("No extra setting is required for this engine."))), engineInfo, 1), true))
else
Ok(views.html.engines.template(handleParamSections[EngineInfo](engineInfo.paramsections, engineInfo, 1), false))
} getOrElse {
NotFound(s"EngineInfo ID ${engine.infoid} not found")
}
}
def getEngineTemplateJs(appid: Int, engineid: Int) = WithEngine(appid, engineid) { (user, app, engine) =>
implicit request =>
Ok(views.js.engines.template(engine.infoid, Seq()))
engineInfos.get(engine.infoid) map { engineInfo =>
Ok(views.js.engines.template(
engineInfo.id,
engineInfo.paramsections.map(collectParams(engineInfo, _)).foldLeft(Set[Param]())(_ ++ _).toSeq))
} getOrElse {
NotFound(s"EngineInfo ID ${engine.infoid} not found")
}
}
def getAlgoSettings(appid: Int, engineid: Int, algoid: Int) = WithAlgo(appid, engineid, algoid) { (user, app, engine, algo) =>
implicit request =>
algoInfos.get(algo.infoid) map { algoInfo =>
// get default params from algoinfo and combined with existing params
val params = algoInfo.params.mapValues(_.defaultvalue) ++ algo.params
Ok(toJson(Map(
"id" -> toJson(algo.id),
"appid" -> toJson(appid),
"engineid" -> toJson(engineid)) ++
(params map { case (k, v) => (k, toJson(v.toString)) })))
} getOrElse {
NotFound(toJson(Map("message" -> toJson(s"Invalid AlgoInfo ID: ${algo.infoid}"))))
}
}
def updateAlgoSettings(appid: Int, engineid: Int, algoid: Int) = WithAlgo(appid, engineid, algoid) { (user, app, engine, algo) =>
implicit request =>
val f = Form(tuple("infoid" -> mapOfStringToAny, "tune" -> text, "tuneMethod" -> text))
f.bindFromRequest.fold(
e => BadRequest(toJson(Map("message" -> toJson(e.toString)))),
bound => {
val (params, tune, tuneMethod) = bound
// NOTE: read-modify-write the original param
val updatedParams = algo.params ++ params ++ Map("tune" -> tune, "tuneMethod" -> tuneMethod) - "infoid"
val updatedAlgo = algo.copy(params = updatedParams)
if (updatedParams("tune") != "auto") {
algos.update(updatedAlgo)
Ok
} else {
// create offline eval with baseline algo
// TODO: get from UI
val defaultBaseLineAlgoType = engine.infoid match {
case "itemrec" => "pdio-randomrank"
case "itemsim" => "pdio-itemsimrandomrank"
}
engineInfos.get(engine.infoid).map { engineInfo =>
val metricinfoid = engineInfo.defaultofflineevalmetricinfoid // TODO: from UI
val splitterinfoid = engineInfo.defaultofflineevalsplitterinfoid // TODO: from UI
algoInfos.get(defaultBaseLineAlgoType).map { baseLineAlgoInfo =>
offlineEvalMetricInfos.get(metricinfoid).map { metricInfo =>
offlineEvalSplitterInfos.get(splitterinfoid).map { splitterInfo =>
// delete previous offlinetune stuff if the algo's offlinetuneid != None
if (updatedAlgo.offlinetuneid != None) {
val tuneid = updatedAlgo.offlinetuneid.get
offlineTunes.get(tuneid) map { tune =>
/** Make sure to unset offline tune's creation time to prevent scheduler from picking up */
offlineTunes.update(tune.copy(createtime = None))
// TODO: check scheduler err
Helper.stopAndDeleteOfflineTuneScheduler(appid.toInt, engineid.toInt, tuneid)
Future {
Helper.deleteOfflineTune(tuneid, keepSettings = false)
}
}
}
// auto tune
algos.update(updatedAlgo)
// create an OfflineTune and paramGen
val offlineTune = OfflineTune(
id = -1,
engineid = updatedAlgo.engineid,
loops = 5, // TODO: default 5 now
createtime = None, // NOTE: no createtime yet
starttime = None,
endtime = None
)
val tuneid = offlineTunes.insert(offlineTune)
Logger.info("Create offline tune ID " + tuneid)
paramGens.insert(ParamGen(
id = -1,
infoid = "random", // TODO: default random scan param gen now
tuneid = tuneid,
params = Map() // TODO: param for param gen
))
// update original algo status to tuning
algos.update(updatedAlgo.copy(
offlinetuneid = Some(tuneid),
status = "tuning"
))
val baseLineAlgo = Algo(
id = -1,
engineid = updatedAlgo.engineid,
name = "Default-BasedLine-Algo-for-OfflineTune-" + tuneid,
infoid = baseLineAlgoInfo.id,
command = "",
params = baseLineAlgoInfo.params.mapValues(_.defaultvalue),
settings = Map(), // no use for now
modelset = false, // init value
createtime = DateTime.now,
updatetime = DateTime.now,
status = "simeval",
offlineevalid = None,
offlinetuneid = Some(tuneid),
loop = Some(0), // loop=0 reserved for autotune baseline
paramset = None
)
val metric = OfflineEvalMetric(
id = 0,
infoid = metricInfo.id,
evalid = 0, // will be assigned later
params = metricInfo.params.mapValues(_.defaultvalue)
)
// percentage param is standard for all splitter
// TODO: hardcode percentage for auto tune for now. get from UI
val percentageParam = Map(
"trainingPercent" -> 0.55,
"validationPercent" -> 0.2,
"testPercent" -> 0.2
)
val splitter = OfflineEvalSplitter(
id = 0,
evalid = 0, // will be assigned later
name = "", // will be assigned later
infoid = splitterInfo.id,
settings = splitterInfo.params.mapValues(_.defaultvalue) ++ percentageParam
)
// TODO: get iterations, metric info, etc from UI, now hardcode to 3.
for (i <- 1 to 3) {
createOfflineEval(engine, List(baseLineAlgo), List(metric), splitter, 1, Some(tuneid))
}
// after everything has setup,
// update with createtime, so scheduler can know it's ready to be picked up
offlineTunes.update(offlineTune.copy(
id = tuneid,
createtime = Some(DateTime.now)
))
// call sync to scheduler here
WS.url(settingsSchedulerUrl + "/users/" + user.id + "/sync").get()
Ok
}.getOrElse(InternalServerError(Json.obj("message" -> s"OfflineEvalSplitterInfo ID ${splitterinfoid} not found.")))
}.getOrElse(InternalServerError(Json.obj("message" -> s"OfflineEvalMetricInfo ID ${metricinfoid} not found.")))
}.getOrElse(InternalServerError(Json.obj("message" -> s"AlgoInfo ID ${defaultBaseLineAlgoType} not found.")))
}.getOrElse(InternalServerError(Json.obj("message" -> s"EngineInfo ID ${engine.infoid} not found.")))
}
})
}
def getAlgoTemplateHtml(appid: Int, engineid: Int, algoid: Int) = WithAlgo(appid, engineid, algoid) { (user, app, engine, algo) =>
implicit request =>
algoInfos.get(algo.infoid) map { algoInfo =>
if (algoInfo.paramsections.isEmpty)
Ok(views.html.algos.template(handleParamSections[AlgoInfo](Seq(ParamSection(
name = "Parameter Settings",
description = Some("No extra setting is required for this algorithm."))), algoInfo, 1), true))
else
Ok(views.html.algos.template(handleParamSections[AlgoInfo](algoInfo.paramsections, algoInfo, 1), false))
} getOrElse {
NotFound(s"AlgoInfo ID ${algo.infoid} not found")
}
}
def getAlgoTemplateJs(appid: Int, engineid: Int, algoid: Int) = WithAlgo(appid, engineid, algoid) { (user, app, engine, algo) =>
implicit request =>
algoInfos.get(algo.infoid) map { algoInfo =>
Ok(views.js.algos.template(
algoInfo.id,
algoInfo.paramsections.map(collectParams(algoInfo, _)).foldLeft(Set[Param]())(_ ++ _).toSeq,
algoInfo.paramsections.map(collectParams(algoInfo, _, true)).foldLeft(Set[Param]())(_ ++ _).toSeq))
} getOrElse {
NotFound(s"AlgoInfo ID ${algo.infoid} not found")
}
}
def getMetricInfoTemplateHtml(engineinfoid: String, metricinfoid: String) = WithUser { user =>
implicit request =>
offlineEvalMetricInfos.get(metricinfoid) map { metricInfo =>
if (metricInfo.paramsections.isEmpty)
Ok(views.html.metrics.template(handleParamSections[OfflineEvalMetricInfo](Seq(ParamSection(
name = "Parameter Settings",
description = Some("No extra setting is required for this metric."))), metricInfo, 1), true))
else
Ok(views.html.metrics.template(handleParamSections[OfflineEvalMetricInfo](metricInfo.paramsections, metricInfo, 1), false))
} getOrElse {
NotFound(s"OfflineEvalMetricInfo ID ${metricinfoid} not found")
}
}
def getSplitterInfoTemplateHtml(engineinfoid: String, splitterinfoid: String) = WithUser { user =>
implicit request =>
offlineEvalSplitterInfos.get(splitterinfoid) map { splitterInfo =>
if (splitterInfo.paramsections.isEmpty)
Ok(views.html.splitters.template(handleParamSections[OfflineEvalSplitterInfo](Seq(ParamSection(
name = "Parameter Settings",
description = Some("No extra setting is required for this splitter."))), splitterInfo, 1), true))
else
Ok(views.html.splitters.template(handleParamSections[OfflineEvalSplitterInfo](splitterInfo.paramsections, splitterInfo, 1), false))
} getOrElse {
NotFound(s"OfflineEvalSplitterInfo ID ${splitterinfoid} not found")
}
}
def collectParams[T <: Info](info: T, paramsection: ParamSection, tuning: Boolean = false): Set[Param] = {
if (tuning)
if (paramsection.sectiontype == "tuning")
paramsection.params.map(_.map(info.params(_)).toSet).getOrElse(Set()) ++
paramsection.subsections.map(_.map(collectParams(info, _, false)).reduce(_ ++ _)).getOrElse(Set())
else
paramsection.subsections.map(_.map(collectParams(info, _, tuning)).reduce(_ ++ _)).getOrElse(Set())
else
paramsection.params.map(_.map(info.params(_)).toSet).getOrElse(Set()) ++
paramsection.subsections.map(_.map(collectParams(info, _, tuning)).reduce(_ ++ _)).getOrElse(Set())
}
def handleParam[T <: Info](param: Param, info: T, tuning: Boolean = false): String = {
val content = param.ui.uitype match {
case "selection" =>
uiSelection[T](info, param.id, param.name, param.description, param.ui.selections.map(_.map(s => (s.name, s.value))).get, Some(param.defaultvalue.toString))
case "slider" =>
uiSlider[T](info, param.id, param.name, param.description)
case _ =>
if (tuning)
uiTextPair[T](info, param.id + "Min", param.id + "Max", param.name, param.description)
else
uiText[T](info, param.id, param.name, param.description, Some(param.defaultvalue.toString))
}
content.toString
}
def handleParamSections[T <: Info](paramsections: Seq[ParamSection], info: T, level: Int, tuning: Boolean = false): String = {
paramsections map { paramsection =>
if (paramsection.sectiontype == "tuning")
views.html.algos.sectionmanualauto(
paramsection.name,
handleParamSectionContent[T](paramsection, info, level),
handleParamSectionContent[T](paramsection, info, level, true))
else if (level > 1)
uiSection2[T](info, paramsection.name, paramsection.description, handleParamSectionContent[T](paramsection, info, level, tuning))
else
uiSection1[T](info, paramsection.name, paramsection.description, handleParamSectionContent[T](paramsection, info, level, tuning))
} mkString ""
}
def handleParamSectionContent[T <: Info](paramsection: ParamSection, info: T, level: Int, tuning: Boolean = false) = {
paramsection.params.map(_.map(p => handleParam[T](info.params(p), info, tuning)).mkString).getOrElse("") +
paramsection.subsections.map(handleParamSections[T](_, info, level + 1, tuning)).getOrElse("")
}
def uiText[T <: Info](info: T, id: String, name: String, description: Option[String], defaultValue: Option[String] = None) = {
info match {
case _: AlgoInfo =>
views.html.algos.text(id, name, description)
case _: EngineInfo =>
views.html.engines.text(id, name, description)
case _: OfflineEvalMetricInfo =>
views.html.metrics.text(id, name, description, defaultValue)
case _: OfflineEvalSplitterInfo =>
views.html.splitters.text(id, name, description, defaultValue)
}
}
def uiTextPair[T <: Info](info: T, id1: String, id2: String, name: String, description: Option[String]) = {
views.html.algos.textpair(id1, id2, name, description)
}
def uiSlider[T <: Info](info: T, id: String, name: String, description: Option[String]) = {
views.html.engines.slider(id, name, description)
}
def uiSelection[T <: Info](info: T, id: String, name: String, description: Option[String], selections: Seq[(String, String)], defaultValue: Option[String] = None) = {
info match {
case _: AlgoInfo =>
views.html.algos.selection(id, name, description, selections)
case _: EngineInfo =>
views.html.engines.selection(id, name, description, selections)
case _: OfflineEvalMetricInfo =>
views.html.metrics.selection(id, name, description, selections, defaultValue)
case _: OfflineEvalSplitterInfo =>
views.html.splitters.selection(id, name, description, selections, defaultValue)
}
}
def uiSection1[T <: Info](info: T, name: String, description: Option[String], content: String) = {
info match {
case _: AlgoInfo =>
views.html.algos.section1(name, description, content)
case _: EngineInfo =>
views.html.engines.section1(name, description, content)
case _: OfflineEvalMetricInfo =>
views.html.metrics.section1(name, description, content)
case _: OfflineEvalSplitterInfo =>
views.html.splitters.section1(name, description, content)
}
}
def uiSection2[T <: Info](info: T, name: String, description: Option[String], content: String) = {
info match {
case _: AlgoInfo =>
views.html.algos.section2(name, description, content)
case _: EngineInfo =>
views.html.engines.section2(name, description, content)
// OfflineEvalMetricInfo doesn't support section2
// OfflineEvalSplitterInfo doesn't support section2
}
}
}