packages feed

bloodhound-1.0.0.0: tests/Test/MLTrainSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.MLTrainSpec (spec) where

import Data.ByteString.Lazy.Char8 qualified as LBS
import Database.Bloodhound.OpenSearch3.Types
import TestsUtils.Import
import Prelude

-- | Sync train response (@POST /_plugins/_ml/_train/{algorithm}@):
-- @{model_id, status}@. The async shape (@{task_id, status}@) is the
-- same 'MLTaskAck' envelope; both decode with one parser.
sampleTrainSync :: LBS.ByteString
sampleTrainSync = "{\"model_id\":\"lblVmX8BO5w8y8RaYYvN\",\"status\":\"COMPLETED\"}"

-- | Async train response: @{task_id, status}@.
sampleTrainAsync :: LBS.ByteString
sampleTrainAsync = "{\"task_id\":\"lrlamX8BO5w8y8Ra2otd\",\"status\":\"CREATED\"}"

-- | Train-predict response: @{status, prediction_result}@. The
-- prediction payload is algorithm-specific, so it is kept opaque.
sampleTrainPredict :: LBS.ByteString
sampleTrainPredict =
  "{\
  \  \"status\": \"COMPLETED\",\
  \  \"prediction_result\": {\
  \    \"column_metas\": [{\"name\": \"ClusterID\", \"column_type\": \"INTEGER\"}],\
  \    \"rows\": [{\"values\": [{\"column_type\": \"INTEGER\", \"value\": 1}]}]\
  \  }\
  \}"

spec :: Spec
spec = describe "ML Commons train / train-predict APIs" $ do
  describe "MLTaskAck (train responses)" $ do
    it "decodes the sync train response (model_id + status)" $
      decode sampleTrainSync
        `shouldBe` Just
          MLTaskAck
            { mlTaskAckTaskId = Nothing,
              mlTaskAckStatus = Just "COMPLETED",
              mlTaskAckModelId = Just "lblVmX8BO5w8y8RaYYvN",
              mlTaskAckTaskType = Nothing
            }
    it "decodes the async train response (task_id + status)" $
      decode sampleTrainAsync
        `shouldBe` Just
          MLTaskAck
            { mlTaskAckTaskId = Just "lrlamX8BO5w8y8Ra2otd",
              mlTaskAckStatus = Just "CREATED",
              mlTaskAckModelId = Nothing,
              mlTaskAckTaskType = Nothing
            }

  describe "TrainModelRequest" $ do
    it "encodes parameters + input_query + input_index, omitting absent input_data" $ do
      let req =
            TrainModelRequest
              { trainModelParameters =
                  object
                    [ "centroids" .= (3 :: Int),
                      "iterations" .= (10 :: Int)
                    ],
                trainModelInputQuery = Just (object ["size" .= (10000 :: Int)]),
                trainModelInputIndex = Just ["iris_data"],
                trainModelInputData = Nothing
              }
      encode req `shouldBe` "{\"input_index\":[\"iris_data\"],\"input_query\":{\"size\":10000},\"parameters\":{\"centroids\":3,\"iterations\":10}}"
    it "round-trips a request built from inline input_data" $ do
      let req =
            TrainModelRequest
              { trainModelParameters = object ["k" .= (2 :: Int)],
                trainModelInputQuery = Nothing,
                trainModelInputIndex = Nothing,
                trainModelInputData = Just (object ["rows" .= ([] :: [Value])])
              }
      decode (encode req) `shouldBe` Just req

  describe "TrainAndPredictResponse" $ do
    it "decodes the doc sample (status + opaque prediction_result)" $ do
      let decoded = decode sampleTrainPredict :: Maybe TrainAndPredictResponse
      case decoded of
        Just r -> do
          trainAndPredictResponseStatus r `shouldBe` Just "COMPLETED"
          trainAndPredictResponsePredictionResult r `shouldSatisfy` isJust
        Nothing -> expectationFailure "expected TrainAndPredictResponse decode"
    it "round-trips a minimal response (status only)" $ do
      let r = TrainAndPredictResponse {trainAndPredictResponseStatus = Just "COMPLETED", trainAndPredictResponsePredictionResult = Nothing}
      decode (encode r) `shouldBe` Just r