shikumi-0.5.0.0: src/Shikumi/Adapter/Prompt.hs
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Prompt records and typed leaves, shared by ordinary and decision rendering.
module Shikumi.Adapter.Prompt (ToPrompt (..), PromptValue (..)) where
import Data.Aeson (Value (..), toJSON)
import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap qualified as KM
import Data.Kind (Type)
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics
import Shikumi.Multimodal (GImageFieldNames (..), GImageFields (..), Image)
import Shikumi.Schema.Types (Constrained (..), Field (..))
class ToPrompt a where
toPromptFields :: a -> [(Text, Text)]
default toPromptFields :: (Generic a, GToPromptFields (Rep a)) => a -> [(Text, Text)]
toPromptFields = gToPromptFields . from
toPrompt :: a -> Text
toPrompt = T.intercalate "\n" . map (\(k, v) -> k <> ": " <> v) . toPromptFields
-- | Structured fields for decision state. Unlike prompt text, strings such as
-- @"null"@ or @"123"@ stay strings, and numbers, lists and optional values keep
-- their JSON types. Override for a custom input representation.
toPromptJSON :: a -> Value
default toPromptJSON :: (Generic a, GToPromptFields (Rep a)) => a -> Value
toPromptJSON = Object . KM.fromList . gToPromptJSON . from
-- | Demonstration JSON retaining rich decision evidence when available.
toEvidenceJSON :: a -> Value
default toEvidenceJSON :: (Generic a, GToPromptFields (Rep a)) => a -> Value
toEvidenceJSON = Object . KM.fromList . gToEvidenceJSON . from
-- | The 'Image' values an /input/ record carries, in field order (EP-24). The
-- adapter lowers these to baikai @UserImage@ blocks in @userTurn@. The generic
-- default walks the record, so any 'Generic' input gets it for free; a record
-- with no image field yields @[]@ and renders byte-for-byte as before. A
-- hand-written instance whose fields are polymorphic (e.g.
-- "Shikumi.Module".@WithReasoning@) cannot be walked generically and overrides
-- this to @[]@.
imageFields :: a -> [Image]
default imageFields :: (Generic a, GImageFields (Rep a)) => a -> [Image]
imageFields = gImageFields . from
-- | The names of an input record's image fields, in field order (EP-24). The
-- text renderer in @userTurn@ drops these so an image is not also rendered as
-- prose. Same defaulting story as 'imageFields'.
imageFieldNames :: a -> [Text]
default imageFieldNames :: (Generic a, GImageFieldNames (Rep a)) => a -> [Text]
imageFieldNames = gImageFieldNames . from
class GToPromptFields (f :: Type -> Type) where
gToPromptFields :: f p -> [(Text, Text)]
gToPromptJSON :: f p -> [(Key.Key, Value)]
gToEvidenceJSON :: f p -> [(Key.Key, Value)]
instance (GToPromptFields cs) => GToPromptFields (D1 d cs) where
gToPromptFields (M1 x) = gToPromptFields x
gToPromptJSON (M1 x) = gToPromptJSON x
gToEvidenceJSON (M1 x) = gToEvidenceJSON x
instance (GToPromptFields cs) => GToPromptFields (C1 c cs) where
gToPromptFields (M1 x) = gToPromptFields x
gToPromptJSON (M1 x) = gToPromptJSON x
gToEvidenceJSON (M1 x) = gToEvidenceJSON x
instance (GToPromptFields a, GToPromptFields b) => GToPromptFields (a :*: b) where
gToPromptFields (a :*: b) = gToPromptFields a ++ gToPromptFields b
gToPromptJSON (a :*: b) = gToPromptJSON a ++ gToPromptJSON b
gToEvidenceJSON (a :*: b) = gToEvidenceJSON a ++ gToEvidenceJSON b
instance (Selector s, PromptValue t) => GToPromptFields (S1 s (K1 R t)) where
gToPromptFields m@(M1 (K1 v)) = [(T.pack (selName m), promptValue v)]
gToPromptJSON m@(M1 (K1 v)) = [(Key.fromString (selName m), promptValueJSON v)]
gToEvidenceJSON m@(M1 (K1 v)) = [(Key.fromString (selName m), promptEvidenceJSON v)]
-- | Render a leaf value to text. Text stays as-is; lists join with commas;
-- 'Maybe' renders its contents or @""@; a 'Shikumi.Schema.Types.Field' unwraps;
-- everything else falls back to 'Show'.
class PromptValue a where
promptValue :: a -> Text
-- | JSON counterpart of the prompt leaf. Custom structured leaves should
-- override this instead of encoding JSON through 'Show'.
promptValueJSON :: a -> Value
promptValueJSON = String . promptValue
-- | Full demonstration evidence; ordinary leaves keep their structured value.
promptEvidenceJSON :: a -> Value
promptEvidenceJSON = promptValueJSON
instance {-# OVERLAPPABLE #-} (Show a) => PromptValue a where
promptValue = T.pack . show
instance PromptValue Text where
promptValue = id
instance PromptValue Bool where
promptValue = T.pack . show
promptValueJSON = toJSON
instance PromptValue Int where
promptValue = T.pack . show
promptValueJSON = toJSON
instance PromptValue Integer where
promptValue = T.pack . show
promptValueJSON = toJSON
instance PromptValue Double where
promptValue = T.pack . show
promptValueJSON = toJSON
instance (PromptValue a) => PromptValue [a] where
promptValue = T.intercalate ", " . map promptValue
promptValueJSON = toJSON . map promptValueJSON
instance (PromptValue a) => PromptValue (Maybe a) where
promptValue = maybe "" promptValue
promptValueJSON = maybe Null promptValueJSON
-- Preserve the existing textual representation of described fields. Decision
-- state uses the unwrapped JSON value without changing legacy prompts.
instance (Show a, PromptValue a) => PromptValue (Field d a) where
promptValue = T.pack . show
promptValueJSON = promptValueJSON . unField
promptEvidenceJSON = promptEvidenceJSON . unField
-- | A t'Constrained' field renders as its inner value (the constraint is a
-- compile-time/decode-time concern, not a prompt concern). EP-26.
instance (PromptValue a) => PromptValue (Constrained cs a) where
promptValue = promptValue . unConstrained
promptValueJSON = promptValueJSON . unConstrained
promptEvidenceJSON = promptEvidenceJSON . unConstrained