diff --git a/Makefile b/Makefile index f883058..51f8038 100644 --- a/Makefile +++ b/Makefile @@ -110,4 +110,4 @@ clean: $(GENERATED_CABAL_FILES) ## test-no-backends # the leftover tests with no particular backend, like Remote Schemas test: - cabal run chat-bots-contrib-test + cabal test all diff --git a/chat-bots-contrib/src/Data/Chat/Bot/Calculator.hs b/chat-bots-contrib/src/Data/Chat/Bot/Calculator.hs index 980b6ae..23ede70 100644 --- a/chat-bots-contrib/src/Data/Chat/Bot/Calculator.hs +++ b/chat-bots-contrib/src/Data/Chat/Bot/Calculator.hs @@ -1,7 +1,13 @@ module Data.Chat.Bot.Calculator - ( calculatorBot, - simplifyCalculatorBot, - printCalcOutput, + ( -- * Bot + calculatorBot, + calculatorBot', + printer, + + -- * Serializer + calculatorSerializer, + + -- * Language module Language, ) where @@ -10,38 +16,33 @@ where import Control.Monad.Reader import Control.Monad.State +import Data.Attoparsec.Text (parseOnly) import Data.Chat.Bot import Data.Chat.Bot.Calculator.Language as Language -import Data.Chat.Bot.Monoidal +import Data.Chat.Bot.Serialization (TextSerializer) +import Data.Chat.Bot.Serialization qualified as S import Data.Chat.Utils -import Data.Profunctor import Data.Text (Text) import Data.Text qualified as Text -------------------------------------------------------------------------------- calculatorBot :: Bot IO CalcState Statement (CalcError \/ CalcResp) -calculatorBot = do - statement <- ask - state $ execCalculator statement - -parseErrorBot :: Monad m => Bot m s ParseError Text -parseErrorBot = pureStatelessBot $ \ParseError {..} -> - "Failed to parse msg: \"" - <> parseInput - <> "\". Error message was: \"" - <> parseError - <> "\"." - -simplifyCalculatorBot :: - Monad m => - Bot m s Statement (CalcError \/ CalcResp) -> - Bot m s Text Text -simplifyCalculatorBot bot = - dimap parseStatement indistinct $ parseErrorBot \/ rmap printCalcOutput bot - -printCalcOutput :: Either CalcError CalcResp -> Text -printCalcOutput = \case +calculatorBot = ask >>= state . execCalculator + +calculatorBot' :: Bot IO CalcState Text Text +calculatorBot' = S.applySerializer calculatorBot calculatorSerializer + +-------------------------------------------------------------------------------- + +calculatorSerializer :: TextSerializer (CalcError \/ CalcResp) Statement +calculatorSerializer = S.Serializer {parser, printer} + +parser :: Text -> Maybe Statement +parser = either (const Nothing) Just . parseOnly statementP + +printer :: Either CalcError CalcResp -> Text +printer = \case Left err -> Text.pack $ show err Right Ack -> "*variable saved*" Right (Log e n) -> Text.pack $ show e <> " = " <> show n diff --git a/chat-bots-contrib/src/Data/Chat/Bot/CoinFlip.hs b/chat-bots-contrib/src/Data/Chat/Bot/CoinFlip.hs index 4fab131..942eade 100644 --- a/chat-bots-contrib/src/Data/Chat/Bot/CoinFlip.hs +++ b/chat-bots-contrib/src/Data/Chat/Bot/CoinFlip.hs @@ -1,13 +1,18 @@ -module Data.Chat.Bot.CoinFlip where +module Data.Chat.Bot.CoinFlip + ( -- * Bot + coinFlipBot, + + -- * Serializer + coinFlipSerializer, + ) +where -------------------------------------------------------------------------------- -import Control.Monad.ListT (emptyListT) -import Control.Monad.Reader import Data.Attoparsec.Text -import Data.Bifunctor (bimap) -import Data.Chat.Bot -import Data.Profunctor +import Data.Chat.Bot (Bot) +import Data.Chat.Bot.Serialization (TextSerializer) +import Data.Chat.Bot.Serialization qualified as S import Data.Text (Text) import Data.Text qualified as Text import System.Random (randomIO) @@ -15,23 +20,15 @@ import System.Random (randomIO) -------------------------------------------------------------------------------- coinFlipBot :: Bot IO () () Bool -coinFlipBot = do - randomIO - -simplifyCoinFlipBot :: forall s. Bot IO s () Bool -> Bot IO s Text Text -simplifyCoinFlipBot b = do - t <- ask - case to t of - Left _err -> Bot $ pure $ const emptyListT - Right _ -> dimap (const ()) from $ b - where - to :: Text -> Either Text () - to = fmap (bimap Text.pack id) $ parseOnly parseCoinFlipCommand - - from :: Bool -> Text - from = \case - True -> "Coin Flip Result: True" - False -> "Coin Flip Result: False" - -parseCoinFlipCommand :: Parser () -parseCoinFlipCommand = "flip a coin" *> pure () +coinFlipBot = randomIO + +-------------------------------------------------------------------------------- + +coinFlipSerializer :: TextSerializer Bool () +coinFlipSerializer = S.Serializer {parser, printer} + +parser :: Text -> Maybe () +parser = either (const Nothing) Just . parseOnly ("flip a coin" *> pure ()) + +printer :: Bool -> Text +printer x = "Coin Flip Result: " <> Text.pack (show x) diff --git a/chat-bots-contrib/src/Data/Chat/Bot/GHCI.hs b/chat-bots-contrib/src/Data/Chat/Bot/GHCI.hs index acc89d7..0c4959c 100644 --- a/chat-bots-contrib/src/Data/Chat/Bot/GHCI.hs +++ b/chat-bots-contrib/src/Data/Chat/Bot/GHCI.hs @@ -1,9 +1,13 @@ {-# LANGUAGE NumDecimals #-} module Data.Chat.Bot.GHCI - ( ghciBot, + ( -- * Bot + ghciBot, ghciConfig, hGetOutput, + + -- * Serializer + ghciSerializer, ) where @@ -12,11 +16,9 @@ where import Control.Monad import Control.Monad.IO.Class import Control.Monad.Loops (whileM) -import Data.Attoparsec.Text as A import Data.Chat.Bot -import Data.Chat.Bot.Monoidal -import Data.Chat.Utils -import Data.Profunctor +import Data.Chat.Bot.Serialization (TextSerializer) +import Data.Chat.Bot.Serialization qualified as S import Data.Text (Text) import Data.Text qualified as Text import GHC.Conc (threadDelay) @@ -25,26 +27,25 @@ import System.Process.Typed -------------------------------------------------------------------------------- -hGetOutput :: Handle -> IO String -hGetOutput handle = whileM (hReady handle) (hGetChar handle) +ghciBot :: Process Handle Handle () -> Bot IO () Text Text +ghciBot p = Bot $ + \s i -> do + o <- liftIO $ do + hPutStrLn (getStdin p) $ Text.unpack i + hFlush (getStdin p) + void $ threadDelay 5e5 + hGetOutput (getStdout p) + pure (Text.pack o, s) -ghciBot' :: Process Handle Handle () -> Bot IO () Text Text -ghciBot' p = - contramapMaybeBot (either (const Nothing) Just . parseOnly ghciInputParser) $ - Bot $ - \s i -> do - o <- liftIO $ do - hPutStrLn (getStdin p) $ Text.unpack i - hFlush (getStdin p) - void $ threadDelay 5e5 - hGetOutput (getStdout p) - pure (Text.pack o, s) +-------------------------------------------------------------------------------- -ghciBot :: Process Handle Handle () -> Bot IO () Text Text -ghciBot p = - dimap (distinguish (/= "ghci: :q")) indistinct $ - pureStatelessBot (const $ "I'm Sorry Dave") - \/ ghciBot' p +ghciSerializer :: TextSerializer Text Text +ghciSerializer = S.prefix "ghci" S.Serializer {parser = pure, printer = id} + +-------------------------------------------------------------------------------- + +hGetOutput :: Handle -> IO String +hGetOutput handle = whileM (hReady handle) (hGetChar handle) ghciConfig :: ProcessConfig Handle Handle () ghciConfig = @@ -52,8 +53,3 @@ ghciConfig = setStdout createPipe $ shell "docker run -i --rm haskell 2>&1" - -ghciInputParser :: Parser Text -ghciInputParser = do - void $ "ghci: " - Text.pack <$> many1 anyChar diff --git a/chat-bots-contrib/src/Data/Chat/Bot/Hello.hs b/chat-bots-contrib/src/Data/Chat/Bot/Hello.hs index 0681c62..4039a42 100644 --- a/chat-bots-contrib/src/Data/Chat/Bot/Hello.hs +++ b/chat-bots-contrib/src/Data/Chat/Bot/Hello.hs @@ -1,31 +1,36 @@ -- | The Simplest Bot. This module serves as an introductory example -- for bot construction. module Data.Chat.Bot.Hello - ( helloSimpleBot, - helloMatrixBot, + ( -- * Bot + helloBot, + + -- * Serializer + helloBotSerializer, + helloBotParser, ) where -------------------------------------------------------------------------------- -import Control.Monad.ListT (emptyListT) import Data.Chat.Bot -import Data.Chat.Server.Matrix +import Data.Chat.Bot.Serialization import Data.Text (Text) import Data.Text qualified as Text -------------------------------------------------------------------------------- --- | A pure, stateless bot which simply takes a 'Text' input and --- produces a 'Text' output from it. -helloSimpleBot :: Monad m => Bot m s Text Text -helloSimpleBot = Bot $ \s msg -> +-- | A pure, stateless bot which produces a 'Text' output. +helloBot :: Monad m => Bot m s () Text +helloBot = Bot $ \s () -> pure ("Are you talking to me, punk?", s) + +-------------------------------------------------------------------------------- + +helloBotParser :: Text -> Maybe () +helloBotParser msg = let name = "cofree-bot" in if name `Text.isInfixOf` msg - then pure ("Are you talking to me, punk?", s) - else emptyListT + then Just () + else Nothing --- | We can then embed our bot in the Matrix API using --- 'liftSimpleBot'. -helloMatrixBot :: Monad m => Bot m () (RoomID, Event) (RoomID, Event) -helloMatrixBot = embedTextBot helloSimpleBot +helloBotSerializer :: TextSerializer Text () +helloBotSerializer = Serializer helloBotParser id diff --git a/chat-bots-contrib/src/Data/Chat/Bot/Jitsi.hs b/chat-bots-contrib/src/Data/Chat/Bot/Jitsi.hs index 94c4469..25a7ff3 100644 --- a/chat-bots-contrib/src/Data/Chat/Bot/Jitsi.hs +++ b/chat-bots-contrib/src/Data/Chat/Bot/Jitsi.hs @@ -1,5 +1,9 @@ module Data.Chat.Bot.Jitsi - ( jitsiBot, + ( -- * Bot + jitsiBot, + + -- * Serializer, + jitsiSerializer, ) where @@ -7,12 +11,24 @@ where import Data.Chat.Bot import Data.Chat.Bot.Jitsi.Dictionary -import Data.Chat.Bot.Monoidal -import Data.Chat.Utils (indistinct) -import Data.Profunctor +import Data.Chat.Bot.Serialization (TextSerializer) +import Data.Chat.Bot.Serialization qualified as S import Data.Text (Text) import Data.Vector qualified as V -import System.Random +import System.Random (randomRIO) + +-------------------------------------------------------------------------------- + +jitsiBot :: Bot IO () () Text +jitsiBot = liftEffect jitsiUrl + +-------------------------------------------------------------------------------- + +jitsiSerializer :: TextSerializer Text () +jitsiSerializer = S.Serializer {parser, printer = id} + +parser :: Text -> Maybe () +parser i = if (i == "🍐" || i == "pair" || i == "pair") then Just () else Nothing -------------------------------------------------------------------------------- @@ -21,21 +37,11 @@ pickRandomElement vs = do i <- randomRIO (0, V.length vs) pure $ vs V.! i -jitsiBot' :: IO Text -jitsiBot' = do +jitsiUrl :: IO Text +jitsiUrl = do adjective <- pickRandomElement adjectives noun <- pickRandomElement pluralNouns verb <- pickRandomElement verbs adverb <- pickRandomElement adverbs let url = "https://meet.jit.si/" <> adjective <> noun <> verb <> adverb pure $ url - -jitsiBot :: Bot IO () Text Text -jitsiBot = - dimap - ( \i -> - if (i == "🍐" || i == "pair" || i == "pair") then Right () else Left () - ) - indistinct - $ emptyBot - \/ liftEffect jitsiBot' diff --git a/chat-bots-contrib/src/Data/Chat/Bot/Magic8Ball.hs b/chat-bots-contrib/src/Data/Chat/Bot/Magic8Ball.hs index 25df8cc..1ee9a6e 100644 --- a/chat-bots-contrib/src/Data/Chat/Bot/Magic8Ball.hs +++ b/chat-bots-contrib/src/Data/Chat/Bot/Magic8Ball.hs @@ -1,19 +1,19 @@ module Data.Chat.Bot.Magic8Ball - ( magic8BallBot, - simplifyMagic8BallBot, + ( -- * Bot + magic8BallBot, + + -- * Serializer + magic8BallSerializer, ) where -------------------------------------------------------------------------------- -import Control.Monad.ListT (emptyListT) -import Control.Monad.Reader import Data.Attoparsec.Text -import Data.Bifunctor (bimap) import Data.Chat.Bot -import Data.Profunctor +import Data.Chat.Bot.Serialization (TextSerializer) +import Data.Chat.Bot.Serialization qualified as S import Data.Text (Text) -import Data.Text qualified as Text import System.Random -------------------------------------------------------------------------------- @@ -22,38 +22,33 @@ magic8BallBot :: Bot IO () () Int magic8BallBot = do randomRIO (1, 20) -simplifyMagic8BallBot :: forall s. Bot IO s () Int -> Bot IO s Text Text -simplifyMagic8BallBot b = do - i <- ask - case to i of - Left _err -> Bot $ pure $ const emptyListT - Right () -> dimap (const ()) from $ b - where - to :: Text -> Either Text () - to = fmap (bimap Text.pack id) $ parseOnly parseMagic8BallCommand - - from :: Int -> Text - from i = case i `mod` 20 of - 1 -> "It is certain." - 2 -> "It is decidedly so." - 3 -> "Without a doubt." - 4 -> "Yes definitely." - 5 -> "You may rely on it." - 6 -> "As I see it, yes." - 7 -> "Most likely." - 8 -> "Outlook good." - 9 -> "Yes." - 10 -> "Signs point to yes." - 11 -> "Reply hazy, try again." - 12 -> "Ask again later." - 13 -> "Better not tell you now." - 14 -> "Cannot predict now." - 15 -> "Concentrate and ask again." - 16 -> "Don't count on it." - 17 -> "My reply is no." - 18 -> "My sources say no." - 19 -> "Outlook not so good." - _ -> "Very doubtful." - -parseMagic8BallCommand :: Parser () -parseMagic8BallCommand = "8 ball" *> pure () +-------------------------------------------------------------------------------- + +magic8BallSerializer :: TextSerializer Int () +magic8BallSerializer = S.Serializer {parser, printer} + +parser :: Text -> Maybe () +parser = either (const Nothing) Just . parseOnly ("8 ball" *> pure ()) + +printer :: Int -> Text +printer i = case i `mod` 20 of + 1 -> "It is certain." + 2 -> "It is decidedly so." + 3 -> "Without a doubt." + 4 -> "Yes definitely." + 5 -> "You may rely on it." + 6 -> "As I see it, yes." + 7 -> "Most likely." + 8 -> "Outlook good." + 9 -> "Yes." + 10 -> "Signs point to yes." + 11 -> "Reply hazy, try again." + 12 -> "Ask again later." + 13 -> "Better not tell you now." + 14 -> "Cannot predict now." + 15 -> "Concentrate and ask again." + 16 -> "Don't count on it." + 17 -> "My reply is no." + 18 -> "My sources say no." + 19 -> "Outlook not so good." + _ -> "Very doubtful." diff --git a/chat-bots-contrib/src/Data/Chat/Bot/Updog.hs b/chat-bots-contrib/src/Data/Chat/Bot/Updog.hs index 259b0ef..241d0e0 100644 --- a/chat-bots-contrib/src/Data/Chat/Bot/Updog.hs +++ b/chat-bots-contrib/src/Data/Chat/Bot/Updog.hs @@ -1,21 +1,46 @@ module Data.Chat.Bot.Updog - ( updogSimpleBot, - updogMatrixBot, + ( -- * Bot + updogBot, + + -- * Serializer + Updog (..), + updogBotParser, + updogSerializer, ) where -------------------------------------------------------------------------------- -import Control.Applicative - ( empty, - liftA2, - ) +import Control.Applicative (liftA2) import Control.Monad.ListT (toListT) import Data.Chat.Bot -import Data.Chat.Server.Matrix +import Data.Chat.Bot.Serialization import Data.String import Data.Text (Text) -import Data.Text qualified as T +import Data.Text qualified as Text + +-------------------------------------------------------------------------------- + +updogBot :: Monad m => Bot m s Updog Text +updogBot = Bot $ \s -> \case + Updog -> toListT [("nothin much whats up with you dog", s), ("HAH GOTTEM", s)] + Snakesay -> toListT [("Hissss, hisssss", s), ("HAH GOTTEM", s)] + OPP -> toListT [("yo, you know me!", s), ("HAH GOTTEM", s)] + +-------------------------------------------------------------------------------- + +data Updog = Updog | Snakesay | OPP + deriving (Show, Read) + +updogBotParser :: Text -> Maybe Updog +updogBotParser msg + | runMatcher (what <> "updog") msg = Just Updog + | runMatcher (what <> "snakesay") msg = Just Snakesay + | runMatcher (what <> "OPP") msg = Just OPP + | otherwise = Nothing + +updogSerializer :: TextSerializer Text Updog +updogSerializer = Serializer updogBotParser id -------------------------------------------------------------------------------- @@ -24,7 +49,7 @@ newtype Matcher = Matcher } instance IsString Matcher where - fromString t = Matcher (T.isInfixOf $ T.pack t) + fromString = Matcher . Text.isInfixOf . Text.pack instance Semigroup Matcher where Matcher p <> Matcher f = Matcher (liftA2 (&&) p f) @@ -35,29 +60,5 @@ instance Monoid Matcher where (|||) :: Matcher -> Matcher -> Matcher Matcher p ||| Matcher f = Matcher $ liftA2 (||) p f -data Match = Match - { mMatch :: Matcher, - mResp :: Text - } - -runMatches :: [Match] -> Text -> [Text] -runMatches ms = flip foldMap ms $ \m t -> case runMatcher (mMatch m) t of - False -> empty - True -> [mResp m, "HAH GOTTEM"] - what :: Matcher what = "what" ||| "What" ||| "WHAT" - --------------------------------------------------------------------------------- - -updogSimpleBot :: Applicative m => Bot m s Text Text -updogSimpleBot = Bot $ \s i -> - let matches = - [ Match (what <> "updog") "nothin much whats up with you dog", - Match (what <> "snakesay") "Hissss, hisssss", - Match (what <> "OPP") "yo, you know me!" - ] - in fmap (,s) $ toListT $ runMatches matches i - -updogMatrixBot :: Monad m => Bot m () (RoomID, Event) (RoomID, Event) -updogMatrixBot = embedTextBot updogSimpleBot diff --git a/chat-bots-contrib/src/Data/Chat/Server/Matrix.hs b/chat-bots-contrib/src/Data/Chat/Server/Matrix.hs index 7938fd3..31d3405 100644 --- a/chat-bots-contrib/src/Data/Chat/Server/Matrix.hs +++ b/chat-bots-contrib/src/Data/Chat/Server/Matrix.hs @@ -2,7 +2,6 @@ module Data.Chat.Server.Matrix ( -- * Matrix Bot MatrixBot, matrix, - simplifyMatrixBot, embedTextBot, RoomID, Event, @@ -14,9 +13,11 @@ where import Control.Lens import Control.Monad.Except import Data.Chat.Bot +import Data.Chat.Bot.Serialization import Data.Chat.Server import Data.Chat.Utils (readFileMaybe) import Data.Map.Strict qualified as Map +import Data.Profunctor import Data.Text (Text) import Data.Text qualified as Text import Network.Matrix.Client @@ -86,16 +87,15 @@ matrix session cache = Server $ do -- Do it again runServer $ go filterId (Just newSince) --- | Map the input and output of a 'MatrixBot' to allow for simple --- 'Text' I/O. -simplifyMatrixBot :: Monad m => MatrixBot m s -> Bot m s Text Text -simplifyMatrixBot (Bot bot) = Bot $ \s i -> do - (responses, nextState) <- bot s (RoomID mempty, mkMsg i) - pure (viewBody $ snd responses, nextState) +embedTextBot :: Applicative m => Bot m s Text Text -> Bot m s (RoomID, Event) (RoomID, Event) +embedTextBot = second' . flip applySerializer eventSerializer -embedTextBot :: Functor m => Bot m s Text Text -> MatrixBot m s -embedTextBot (Bot bot) = Bot $ \s (rid, i) -> - fmap (\(i', s') -> ((rid, mkMsg i'), s')) $ bot s (viewBody i) +eventSerializer :: Serializer Event Event Text Text +eventSerializer = + Serializer + { parser = pure . viewBody, + printer = mkMsg + } viewBody :: Event -> Text viewBody = view (_EventRoomMessage . _RoomMessageText . _mtBody) diff --git a/chat-bots-contrib/test/Spec.hs b/chat-bots-contrib/test/Spec.hs index fd19067..00e1e1d 100644 --- a/chat-bots-contrib/test/Spec.hs +++ b/chat-bots-contrib/test/Spec.hs @@ -7,13 +7,9 @@ module Main where import Data.Chat.Bot (Behavior, Bot (..), fixBot) import Data.Chat.Bot.Calculator - ( calculatorBot, - printCalcOutput, - simplifyCalculatorBot, - ) -import Data.Chat.Bot.Calculator.Language (statementP) -import Data.Chat.Bot.Context (sessionize, simplifySessionBot) -import Data.Chat.Bot.Hello (helloSimpleBot) +import Data.Chat.Bot.Context (sessionSerializer, sessionize) +import Data.Chat.Bot.Hello +import Data.Chat.Bot.Serialization qualified as S import Data.Text (Text, pack) import Scripts (Script, mkScript) import Test.Hspec (Spec, describe, hspec, it, shouldNotBe) @@ -57,7 +53,7 @@ scriptedTestsSpec = describe "Scripted tests" $ do helloBotSpec :: Spec helloBotSpec = describe "Hello Bot" $ do - let bot = helloSimpleBot + let bot = S.applySerializer helloBot helloBotSerializer it "responds to precisely its trigger phrase" $ do fixBot bot () `conformsToScript` [mkScript| @@ -75,7 +71,7 @@ helloBotSpec = calculatorBotSpec :: Spec calculatorBotSpec = describe "Calculator Bot" $ do - let bot = simplifyCalculatorBot calculatorBot + let bot = S.applySerializer calculatorBot calculatorSerializer it "performs arithmetic" $ do fixBot bot mempty `conformsToScript` [mkScript| @@ -99,7 +95,7 @@ calculatorBotSpec = sessionizedBotSpec :: Spec sessionizedBotSpec = describe "Sessionized Bot" $ do - let bot = simplifySessionBot printCalcOutput statementP $ sessionize mempty $ calculatorBot + let bot = S.applySerializer (sessionize mempty calculatorBot) (sessionSerializer calculatorSerializer) it "can instantiate a session" $ do fixBot bot mempty `conformsToScript` [mkScript| diff --git a/chat-bots/chat-bots.cabal b/chat-bots/chat-bots.cabal index 1dffe36..1ecdde5 100644 --- a/chat-bots/chat-bots.cabal +++ b/chat-bots/chat-bots.cabal @@ -68,11 +68,11 @@ library Data.Chat.Bot Data.Chat.Bot.Monoidal Data.Chat.Bot.Context + Data.Chat.Bot.Serialization Data.Chat.Server Data.Chat.Server.Repl Data.Chat.Utils Control.Monad.ListT - Parsing build-depends: , aeson diff --git a/chat-bots/src/Data/Chat/Bot/Context.hs b/chat-bots/src/Data/Chat/Bot/Context.hs index cbf08e5..9513ffd 100644 --- a/chat-bots/src/Data/Chat/Bot/Context.hs +++ b/chat-bots/src/Data/Chat/Bot/Context.hs @@ -18,25 +18,23 @@ module Data.Chat.Bot.Context SessionInput (..), SessionOutput (..), sessionize, - simplifySessionBot, + sessionSerializer, ) where -------------------------------------------------------------------------------- import Control.Applicative -import Control.Arrow qualified as Arrow -import Control.Monad.ListT (emptyListT) import Data.Attoparsec.Text -import Data.Bifunctor (Bifunctor (first)) import Data.Chat.Bot -import Data.IntMap.Strict (IntMap) +import Data.Chat.Bot.Serialization (TextSerializer) +import Data.Chat.Bot.Serialization qualified as S +import Data.IntMap (IntMap) import Data.IntMap.Strict qualified as IntMap -import Data.Kind (Type) import Data.Profunctor (second') import Data.Text (Text) import Data.Text qualified as Text -import Network.Matrix.Client +import Network.Matrix.Client (RoomID, UserID) -------------------------------------------------------------------------------- @@ -63,21 +61,6 @@ mkUserAware = second' -------------------------------------------------------------------------------- --- | A map of states @s@ used to track sessions in a "sessionized" bot. -newtype SessionState s = SessionState {sessions :: IntMap s} - deriving newtype (Show, Read, Semigroup, Monoid) - -freshSessionKey :: IntMap a -> Int -freshSessionKey state = case IntMap.lookupMax state of - Nothing -> 0 - Just (k, _) -> k + 1 - --- | Expand the input type @i@ to include session interaction meta commands. -data SessionInput i = InteractWithSession Int i | StartSession | EndSession Int - --- | Expand the output type @o@ to include session interaction meta commands. -data SessionOutput o = SessionOutput Int o | SessionStarted Int | SessionEnded Int | InvalidSession Int - -- | Enable sessions for a 'Bot'. -- -- A sessionized 'Bot' can be interacted with using the commands @new@, @use@, and @end@: @@ -111,48 +94,56 @@ sessionize defaultState (Bot bot) = Bot $ \(SessionState s) si -> case si of (SessionOutput k responses) (SessionState $ IntMap.insert k nextState s) +-------------------------------------------------------------------------------- + +-- | A map of states @s@ used to track sessions in a "sessionized" bot. +newtype SessionState s = SessionState {sessions :: IntMap s} + deriving newtype (Show, Read, Semigroup, Monoid) + +freshSessionKey :: IntMap a -> Int +freshSessionKey state = case IntMap.lookupMax state of + Nothing -> 0 + Just (k, _) -> k + 1 + +-- | Expand the input type @i@ to include session interaction meta commands. +data SessionInput i = InteractWithSession Int i | StartSession | EndSession Int + +-- | Expand the output type @o@ to include session interaction meta commands. +data SessionOutput o = SessionOutput Int o | SessionStarted Int | SessionEnded Int | InvalidSession Int + +-------------------------------------------------------------------------------- + +sessionSerializer :: TextSerializer o i -> TextSerializer (SessionOutput o) (SessionInput i) +sessionSerializer S.Serializer {parser = parser', printer = printer'} = + S.Serializer {parser = parser parser', printer = printer printer'} + +printer :: (o -> Text) -> SessionOutput o -> Text +printer p = \case + SessionOutput n o -> + "Session '" <> Text.pack (show n) <> "' Output:\n" <> p o + SessionStarted n -> "Session Started: '" <> Text.pack (show n) <> "'." + SessionEnded n -> "Session Ended: '" <> Text.pack (show n) <> "'." + InvalidSession n -> "Invalid Session: '" <> Text.pack (show n) <> "'." + +parser :: (Text -> Maybe i) -> Text -> Maybe (SessionInput i) +parser p = either (const Nothing) Just . parseOnly (sessionizedParser p) + data Nue = New | Use | End -parseSessionInfo :: Parser i -> Parser (SessionInput i) -parseSessionInfo p = do +sessionizedParser :: (Text -> Maybe i) -> Parser (SessionInput i) +sessionizedParser p = do keyword <- New <$ "new" <|> Use <$ "use" <|> End <$ "end" case keyword of New -> pure StartSession Use -> do _ <- space n <- decimal <* ": " - i <- p + mi <- fmap p takeText -- endOfLine - pure $ InteractWithSession n i + case mi of + Just i -> pure $ InteractWithSession n i + Nothing -> fail "bad parse" End -> do _ <- space n <- decimal pure $ EndSession n - --- | Sessionized bots require a parsable input and printable output. --- --- Given a printer @o -> Text@ and a @Parser i@, convert the --- sessionized bot into a 'Bot m s Text Text' which can then be further composed --- with other bots. -simplifySessionBot :: - forall m s i o. - (Show s, Monad m) => - (o -> Text) -> - Parser i -> - Bot m s (SessionInput i) (SessionOutput o) -> - Bot m s Text Text -simplifySessionBot tshow p (Bot bot) = Bot $ \s i -> do - case to i of - Left _ -> emptyListT - Right si -> fmap (Arrow.first from) $ bot s si - where - to :: Text -> Either Text (SessionInput i) - to = fmap (first Text.pack) $ parseOnly $ parseSessionInfo p - - from :: SessionOutput o -> Text - from = \case - SessionOutput n o -> - "Session '" <> Text.pack (show n) <> "' Output:\n" <> tshow o - SessionStarted n -> "Session Started: '" <> Text.pack (show n) <> "'." - SessionEnded n -> "Session Ended: '" <> Text.pack (show n) <> "'." - InvalidSession n -> "Invalid Session: '" <> Text.pack (show n) <> "'." diff --git a/chat-bots/src/Data/Chat/Bot/Serialization.hs b/chat-bots/src/Data/Chat/Bot/Serialization.hs new file mode 100644 index 0000000..c6e1be9 --- /dev/null +++ b/chat-bots/src/Data/Chat/Bot/Serialization.hs @@ -0,0 +1,63 @@ +-- | Bidirectional parsing to map 'Bot' I/O to 'Server' I/O. +module Data.Chat.Bot.Serialization where + +-------------------------------------------------------------------------------- + +import Control.Applicative (liftA2) +import Control.Monad ((>=>)) +import Control.Monad.ListT (emptyListT) +import Data.Attoparsec.Text qualified as P +import Data.Bifunctor (first) +import Data.Chat.Bot (Bot (..)) +import Data.Chat.Utils (can, type (/+\)) +import Data.Text (Text) +import Data.These (These (..), these) + +-------------------------------------------------------------------------------- + +applySerializer :: + Applicative m => + Bot m s bi bo -> + Serializer so si bo bi -> + Bot m s so si +applySerializer (Bot bot) (Serializer parser printer) = Bot $ \s i -> + case parser i of + Nothing -> emptyListT + Just i' -> first printer <$> bot s i' + +-------------------------------------------------------------------------------- + +-- | Bidirectional serializer from 'Server' I/O to 'Bot' I/O. +data Serializer so si bo bi = Serializer + {parser :: so -> Maybe bi, printer :: bo -> si} + +-- | A 'Serializer' whose 'Server' I/O has been specialized to 'Text'. +type TextSerializer = Serializer Text Text + +-- | Extend the parser portion of a 'TextSerializer' to consume a +-- prefix string. +prefix :: Text -> TextSerializer x y -> TextSerializer x y +prefix prefix' Serializer {..} = + let prefixParser = P.string prefix' *> ":" *> P.skipSpace *> P.takeText + parsePrefix = either (const Nothing) Just . P.parseOnly prefixParser + in Serializer + { parser = parsePrefix >=> parser, + printer = printer + } + +infixr 6 /+\ + +(/+\) :: TextSerializer o i -> TextSerializer o' i' -> TextSerializer (o /+\ o') (i /+\ i') +(/+\) (Serializer par1 pri1) (Serializer par2 pri2) = Serializer (par1 *|* par2) (pri1 +|+ pri2) + +-- | Parse and tensor the components @a@ and @b@ of a 'These'. +infixr 6 *|* + +(*|*) :: (Text -> Maybe a) -> (Text -> Maybe b) -> Text -> Maybe (a /+\ b) +(*|*) p1 p2 = these (fmap This) (fmap That) can . liftA2 These p1 p2 + +-- | Print and combine the components @a@ and @b@ of a 'These'. +infixr 6 +|+ + +(+|+) :: (a -> Text) -> (b -> Text) -> a /+\ b -> Text +(+|+) p1 p2 = these p1 p2 $ \a b -> p1 a <> "\n" <> p2 b diff --git a/chat-bots/src/Parsing.hs b/chat-bots/src/Parsing.hs deleted file mode 100644 index 4b7a7a4..0000000 --- a/chat-bots/src/Parsing.hs +++ /dev/null @@ -1,20 +0,0 @@ -module Parsing where - -import Data.Void (Void) - -class Invariant f where - invmap :: (b -> a) -> (a -> b) -> f a -> f b - -class Invariant f => Invariant' f where - unit1 :: f () - unit0 :: f Void - (<×>) :: f a -> f b -> f (a, b) - (<+>) :: f a -> f b -> f (Either a b) - in1 :: f a -> (f a, f b) - in2 :: f b -> (f a, f b) - - lstrong1 :: - (a, f b) -> f (a, b) - - lstrong2 :: - (a, f (a, b)) -> f b diff --git a/cofree-bot/app/Main.hs b/cofree-bot/app/Main.hs index 6c0e020..c467a08 100644 --- a/cofree-bot/app/Main.hs +++ b/cofree-bot/app/Main.hs @@ -1,11 +1,15 @@ {-# LANGUAGE NumDecimals #-} +{-# OPTIONS_GHC -Wno-missing-signatures #-} module Main where -------------------------------------------------------------------------------- -import Control.Monad (void, (>=>)) -import Control.Monad.Except (ExceptT, runExceptT) +import Control.Monad +import Control.Monad.Except + ( ExceptT, + runExceptT, + ) import Control.Monad.IO.Class (liftIO) import Data.Chat.Bot import Data.Chat.Bot.Calculator @@ -16,6 +20,7 @@ import Data.Chat.Bot.Hello import Data.Chat.Bot.Jitsi import Data.Chat.Bot.Magic8Ball import Data.Chat.Bot.Monoidal +import Data.Chat.Bot.Serialization qualified as S import Data.Chat.Bot.Updog import Data.Chat.Server import Data.Chat.Server.Matrix @@ -30,6 +35,8 @@ import System.Process.Typed (getStdout, withProcessWait_) -------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- + main :: IO () main = do xdgCache <- getUserCacheDir "cofree-bot" @@ -51,23 +58,29 @@ main = do -------------------------------------------------------------------------------- -bot process = - let calcBot = - embedTextBot $ - simplifySessionBot printCalcOutput statementP $ - sessionize mempty $ - calculatorBot - helloBot = helloMatrixBot - coinFlipBot' = embedTextBot $ simplifyCoinFlipBot coinFlipBot - ghciBot' = embedTextBot $ ghciBot process - magic8BallBot' = embedTextBot $ simplifyMagic8BallBot magic8BallBot - in calcBot - /.\ coinFlipBot' - /.\ helloBot - /.\ ghciBot' - /.\ magic8BallBot' - /.\ updogMatrixBot - /.\ embedTextBot jitsiBot +-------------------------------------------------------------------------------- + +bot' process = + helloBot @_ @() -- <----- polymorphic states need to get asserted to a monoid + /+\ updogBot @_ @() + /+\ coinFlipBot + /+\ magic8BallBot + /+\ jitsiBot + /+\ ghciBot process + /+\ sessionize mempty calculatorBot + +serializer' = + helloBotSerializer + S./+\ updogSerializer + S./+\ coinFlipSerializer + S./+\ magic8BallSerializer + S./+\ jitsiSerializer + S./+\ ghciSerializer + S./+\ sessionSerializer calculatorSerializer + +bot process = S.applySerializer (bot' process) serializer' + +-------------------------------------------------------------------------------- -------------------------------------------------------------------------------- @@ -76,7 +89,7 @@ cliMain xdgCache = withProcessWait_ ghciConfig $ \process -> do void $ threadDelay 1e6 void $ hGetOutput (getStdout process) state <- readState xdgCache - fixedBot <- flip (fixBotPersistent xdgCache) (fold state) $ simplifyMatrixBot $ bot process + fixedBot <- flip (fixBotPersistent xdgCache) (fold state) $ bot process void $ loop $ annihilate repl fixedBot -------------------------------------------------------------------------------- @@ -89,5 +102,5 @@ matrixMain session xdgCache = withProcessWait_ ghciConfig $ \process -> do void $ threadDelay 1e6 void $ hGetOutput (getStdout process) state <- readState xdgCache - fixedBot <- flip (fixBotPersistent xdgCache) (fold state) $ hoistBot liftIO $ bot process - unsafeCrashInIO $ loop $ annihilate (matrix session xdgCache) $ batch fixedBot + fixedBot <- flip (fixBotPersistent xdgCache) (fold state) $ embedTextBot $ hoistBot liftIO $ bot process + unsafeCrashInIO $ loop $ annihilate (matrix session xdgCache) $ batch $ fixedBot