packages feed

ollama-haskell-0.4.2.0: src/Ollama/API/SystemOne.hs

{- |
Module      : Ollama.API.SystemOne
Copyright   : (c) 2024-2026 Tushar Adhatrao
License     : MIT
Maintainer  : tusharadhatrao@gmail.com
Stability   : stable
Portability : portable

System One decision evaluation API endpoint (@/v1/systemone@).

Evaluates high-speed classification, probability, and scoring questions
with specialized decision models (e.g. @nimble@).

@since 0.4.2.0
-}
module Ollama.API.SystemOne (
  -- * Client Function
  systemOne,

  -- * Request & Response
  SystemOneRequest (..),
  systemOneRequest,
  SystemOneResponse (..),
  SystemOneUsage (..),

  -- * Content
  SystemOneContent (..),

  -- * Questions
  SystemOneQuestion (..),
  questionInstructions,
  choiceQuestion,
  noulQuestion,
  noulQuestionWithCriteria,
  scoreQuestion,

  -- * Answers
  SystemOneAnswer (..),
  ChoiceAnswer (..),
  ScoreAnswer (..),
) where

import Control.Monad.IO.Class (MonadIO)
import Data.Aeson
import Data.Map.Strict (Map)
import Data.Maybe (catMaybes)
import Data.String (IsString (..))
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics (Generic)
import Ollama.Client (OllamaClient)
import Ollama.Client.Internal (request)
import Ollama.Error (OllamaError)
import Ollama.Types.Common (ModelName)

{- | State or context payload passed to System One, represented as text or structured JSON.

@since 0.4.2.0
-}
data SystemOneContent
  = StateText !Text
  | StateJson !Value
  deriving stock (Eq, Show, Generic)

instance IsString SystemOneContent where
  fromString = StateText . T.pack

instance ToJSON SystemOneContent where
  toJSON (StateText t) = String t
  toJSON (StateJson v) = v

instance FromJSON SystemOneContent where
  parseJSON (String t) = pure (StateText t)
  parseJSON v = pure (StateJson v)

{- | Specification of a question to evaluate against the state.

@since 0.4.2.0
-}
data SystemOneQuestion
  = ChoiceQuestion !SystemOneContent !(Map Text (Maybe Text))
  | NoulQuestion !SystemOneContent !(Maybe (Map Text Text))
  | ScoreQuestion !SystemOneContent ![Text]
  deriving stock (Eq, Show, Generic)

-- | Extract the instructions from any question variant.
questionInstructions :: SystemOneQuestion -> SystemOneContent
questionInstructions (ChoiceQuestion inst _) = inst
questionInstructions (NoulQuestion inst _) = inst
questionInstructions (ScoreQuestion inst _) = inst

instance ToJSON SystemOneQuestion where
  toJSON (ChoiceQuestion inst criteria) =
    object
      [ "type" .= ("choice" :: Text)
      , "instructions" .= inst
      , "criteria" .= criteria
      ]
  toJSON (NoulQuestion inst criteria) =
    object $
      [ "type" .= ("noul" :: Text)
      , "instructions" .= inst
      ]
        ++ maybe [] (\c -> ["criteria" .= c]) criteria
  toJSON (ScoreQuestion inst criteria) =
    object
      [ "type" .= ("score" :: Text)
      , "instructions" .= inst
      , "criteria" .= criteria
      ]

instance FromJSON SystemOneQuestion where
  parseJSON = withObject "SystemOneQuestion" $ \v -> do
    qType <- v .: "type"
    case (qType :: Text) of
      "choice" -> ChoiceQuestion <$> v .: "instructions" <*> v .: "criteria"
      "noul" -> NoulQuestion <$> v .: "instructions" <*> v .:? "criteria"
      "score" -> ScoreQuestion <$> v .: "instructions" <*> v .: "criteria"
      other -> fail $ "Unknown SystemOne question type: " <> T.unpack other

{- | Smart constructor for a multiple-choice question with candidate descriptions.

@since 0.4.2.0
-}
choiceQuestion :: SystemOneContent -> Map Text (Maybe Text) -> SystemOneQuestion
choiceQuestion = ChoiceQuestion

{- | Smart constructor for a binary probability (noul) question.

@since 0.4.2.0
-}
noulQuestion :: SystemOneContent -> SystemOneQuestion
noulQuestion instructions = NoulQuestion instructions Nothing

{- | Smart constructor for a binary probability (noul) question with custom true\/false descriptions.

@since 0.4.2.0
-}
noulQuestionWithCriteria :: SystemOneContent -> Map Text Text -> SystemOneQuestion
noulQuestionWithCriteria instructions criteria = NoulQuestion instructions (Just criteria)

{- | Smart constructor for an ordinal scoring question with descriptions from lowest to highest score.

@since 0.4.2.0
-}
scoreQuestion :: SystemOneContent -> [Text] -> SystemOneQuestion
scoreQuestion = ScoreQuestion

{- | Result of a multiple-choice question evaluation.

@since 0.4.2.0
-}
data ChoiceAnswer = ChoiceAnswer
  { choiceResult :: !Text
  , choiceProbabilities :: !(Map Text Double)
  , choiceConfidence :: !Double
  }
  deriving stock (Eq, Show, Generic)

instance FromJSON ChoiceAnswer where
  parseJSON = withObject "ChoiceAnswer" $ \v ->
    ChoiceAnswer
      <$> v .: "choice"
      <*> v .: "probabilities"
      <*> v .: "confidence"

instance ToJSON ChoiceAnswer where
  toJSON ChoiceAnswer {..} =
    object
      [ "choice" .= choiceResult
      , "probabilities" .= choiceProbabilities
      , "confidence" .= choiceConfidence
      ]

{- | Result of an ordinal scoring question evaluation.

@since 0.4.2.0
-}
data ScoreAnswer = ScoreAnswer
  { scoreValue :: !Double
  , scoreLegend :: !(Map Text Text)
  , scoreProbabilities :: !(Map Text Double)
  , scoreConfidence :: !Double
  }
  deriving stock (Eq, Show, Generic)

instance FromJSON ScoreAnswer where
  parseJSON = withObject "ScoreAnswer" $ \v ->
    ScoreAnswer
      <$> v .: "score"
      <*> v .: "legend"
      <*> v .: "probabilities"
      <*> v .: "confidence"

instance ToJSON ScoreAnswer where
  toJSON ScoreAnswer {..} =
    object
      [ "score" .= scoreValue
      , "legend" .= scoreLegend
      , "probabilities" .= scoreProbabilities
      , "confidence" .= scoreConfidence
      ]

{- | Answer returned by the model for a question.

@since 0.4.2.0
-}
data SystemOneAnswer
  = AnswerChoice !ChoiceAnswer
  | AnswerNoul !Double
  | AnswerScore !ScoreAnswer
  deriving stock (Eq, Show, Generic)

instance FromJSON SystemOneAnswer where
  parseJSON = withObject "SystemOneAnswer" $ \v -> do
    aType <- v .: "type"
    case (aType :: Text) of
      "choice" -> AnswerChoice <$> parseJSON (Object v)
      "noul" -> AnswerNoul <$> v .: "noul"
      "score" -> AnswerScore <$> parseJSON (Object v)
      other -> fail $ "Unknown SystemOne answer type: " <> T.unpack other

instance ToJSON SystemOneAnswer where
  toJSON (AnswerChoice ChoiceAnswer {..}) =
    object
      [ "type" .= ("choice" :: Text)
      , "choice" .= choiceResult
      , "probabilities" .= choiceProbabilities
      , "confidence" .= choiceConfidence
      ]
  toJSON (AnswerNoul p) =
    object
      [ "type" .= ("noul" :: Text)
      , "noul" .= p
      ]
  toJSON (AnswerScore ScoreAnswer {..}) =
    object
      [ "type" .= ("score" :: Text)
      , "score" .= scoreValue
      , "legend" .= scoreLegend
      , "probabilities" .= scoreProbabilities
      , "confidence" .= scoreConfidence
      ]

{- | Token usage statistics for System One requests.

@since 0.4.2.0
-}
data SystemOneUsage = SystemOneUsage
  { souInputTokens :: !Int
  , souOutputTokens :: !Int
  }
  deriving stock (Eq, Show, Generic)

instance FromJSON SystemOneUsage where
  parseJSON = withObject "SystemOneUsage" $ \v ->
    SystemOneUsage <$> v .: "input_tokens" <*> v .: "output_tokens"

instance ToJSON SystemOneUsage where
  toJSON SystemOneUsage {..} =
    object
      [ "input_tokens" .= souInputTokens
      , "output_tokens" .= souOutputTokens
      ]

{- | Request payload for the System One evaluation endpoint.

@since 0.4.2.0
-}
data SystemOneRequest = SystemOneRequest
  { soModel :: !ModelName
  , soState :: !SystemOneContent
  , soQuestions :: !(Map Text SystemOneQuestion)
  , soKeepAlive :: !(Maybe Text)
  }
  deriving stock (Eq, Show, Generic)

instance ToJSON SystemOneRequest where
  toJSON SystemOneRequest {..} =
    object $
      catMaybes
        [ Just ("model" .= soModel)
        , Just ("state" .= soState)
        , Just ("questions" .= soQuestions)
        , ("keep_alive" .=) <$> soKeepAlive
        ]

instance FromJSON SystemOneRequest where
  parseJSON = withObject "SystemOneRequest" $ \v ->
    SystemOneRequest
      <$> v .: "model"
      <*> v .: "state"
      <*> v .: "questions"
      <*> v .:? "keep_alive"

{- | Smart constructor for a 'SystemOneRequest'.

@since 0.4.2.0
-}
systemOneRequest :: ModelName -> SystemOneContent -> Map Text SystemOneQuestion -> SystemOneRequest
systemOneRequest model state questions =
  SystemOneRequest
    { soModel = model
    , soState = state
    , soQuestions = questions
    , soKeepAlive = Nothing
    }

{- | Response payload returned by the System One endpoint.

@since 0.4.2.0
-}
data SystemOneResponse = SystemOneResponse
  { sorModel :: !ModelName
  , sorAnswers :: !(Map Text SystemOneAnswer)
  , sorUsage :: !SystemOneUsage
  }
  deriving stock (Eq, Show, Generic)

instance FromJSON SystemOneResponse where
  parseJSON = withObject "SystemOneResponse" $ \v ->
    SystemOneResponse
      <$> v .: "model"
      <*> v .: "answers"
      <*> v .: "usage"

instance ToJSON SystemOneResponse where
  toJSON SystemOneResponse {..} =
    object
      [ "model" .= sorModel
      , "answers" .= sorAnswers
      , "usage" .= sorUsage
      ]

{- | Score and evaluate choice, noul, and score questions with a local System One model (@POST /v1/systemone@).

@since 0.4.2.0
-}
systemOne ::
  (MonadIO m) =>
  OllamaClient ->
  SystemOneRequest ->
  m (Either OllamaError SystemOneResponse)
systemOne client req = request client "POST" "/v1/systemone" (Just req)