intelli-monad-0.1.2.0: src/IntelliMonad/Repl.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module IntelliMonad.Repl where
import Control.Monad (forM_)
import Control.Monad.IO.Class
import Control.Monad.Trans.Class (MonadTrans, lift)
import Control.Monad.Trans.State (get, put)
import qualified Data.Aeson as A
import Data.Aeson.Encode.Pretty (encodePretty)
import qualified Data.Yaml as Y
import qualified Data.Yaml.Pretty as Y
import qualified Data.ByteString as BS
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.Void
import GHC.IO.Exception
import IntelliMonad.Persist
import IntelliMonad.Prompt hiding (user, system, assistant)
import IntelliMonad.Types
import qualified Louter.Types.Request as Louter
import System.Console.Haskeline
import System.Environment (lookupEnv)
import System.IO (hClose)
import System.IO.Temp
import System.Process
import Text.Megaparsec
import Text.Megaparsec.Char
import Text.Megaparsec.Char.Lexer as L
import IntelliMonad.Config
type Parser = Parsec Void Text
parseRepl :: Parser ReplCommand
parseRepl =
(try (lexm (string ":quit")) >> pure Quit)
<|> (try (lexm (string ":clear")) >> pure Clear)
<|> (try (lexm (string ":show") >> lexm (string "contents")) >> pure ShowContents)
<|> (try (lexm (string ":show") >> lexm (string "usage")) >> pure ShowUsage)
<|> (try (lexm (string ":show") >> lexm (string "request")) >> pure ShowRequest)
<|> (try (lexm (string ":show") >> lexm (string "context")) >> pure ShowContext)
<|> (try (lexm (string ":show") >> lexm (string "session")) >> pure ShowSession)
<|> (try (lexm (string ":read") >> lexm (string "image") >> lexm imagePath) >>= pure . ReadImage . T.pack)
<|> (try (lexm (string ":list") >> lexm (string "sessions")) >> pure ListSessions)
<|> (try (lexm (string ":list")) >> pure ListSessions)
<|> ( try
( lexm (string ":copy") >> lexm (string "session") >> do
from <- T.pack <$> lexm sessionName
to <- T.pack <$> lexm sessionName
return $ CopySession from to
)
)
<|> (try (lexm (string ":delete") >> lexm (string "session") >> lexm sessionName) >>= pure . DeleteSession . T.pack)
<|> (try (lexm (string ":switch") >> lexm (string "session") >> lexm sessionName) >>= pure . SwitchSession . T.pack)
<|> (try (lexm (string ":help")) >> pure Help)
<|> (try (lexm (string ":edit") >> lexm (string "request")) >> pure EditRequest)
<|> (try (lexm (string ":edit") >> lexm (string "contents")) >> pure EditContents)
<|> (try (lexm (string ":edit") >> lexm (string "header")) >> pure EditHeader)
<|> (try (lexm (string ":edit") >> lexm (string "footer")) >> pure EditFooter)
<|> (try (lexm (string ":edit")) >> pure Edit)
where
sc = L.space space1 empty empty
lexm = lexeme sc
sessionName = many alphaNumChar
imagePath = many (alphaNumChar <|> char '.' <|> char '/' <|> char '-')
getTextInputLine :: (MonadTrans t) => t (InputT IO) (Maybe T.Text)
getTextInputLine = fmap (fmap T.pack) (lift $ getInputLine "% ")
getUserCommand :: forall p t. (PersistentBackend p, MonadTrans t) => t (InputT IO) (Either (ParseErrorBundle Text Void) ReplCommand)
getUserCommand = do
minput <- getTextInputLine
case minput of
Nothing -> return $ Right Quit
Just input ->
if T.isPrefixOf ":" input
then case runParser parseRepl "stdin" input of
Right v -> return $ Right v
Left err -> return $ Left err
else return $ Right (UserInput input)
editWithEditor :: forall m. (MonadIO m, MonadFail m) => m (Maybe T.Text)
editWithEditor = do
liftIO $ withSystemTempFile "tempfile.txt" $ \filePath fileHandle -> do
hClose fileHandle
editor <- do
lookupEnv "EDITOR" >>= \case
Just editor' -> return editor'
Nothing -> return "vim"
code <- system (editor <> " " <> filePath)
case code of
ExitSuccess -> Just <$> T.readFile filePath
ExitFailure _ -> return Nothing
editRequestWithEditor :: forall m. (MonadIO m, MonadFail m) => Louter.ChatRequest -> m (Maybe Louter.ChatRequest)
editRequestWithEditor req = do
liftIO $ withSystemTempFile "tempfile.yaml" $ \filePath fileHandle -> do
hClose fileHandle
BS.writeFile filePath $ Y.encodePretty Y.defConfig req
editor <- do
lookupEnv "EDITOR" >>= \case
Just editor' -> return editor'
Nothing -> return "vim"
code <- system (editor <> " " <> filePath)
case code of
ExitSuccess -> do
newReq <- Y.decodeFileEither @Louter.ChatRequest filePath
case newReq of
Right newReq' -> return $ Just newReq'
Left err -> do
print err
return Nothing
ExitFailure _ -> return Nothing
editContentsWithEditor :: forall m. (MonadIO m, MonadFail m) => Contents -> m (Maybe Contents)
editContentsWithEditor contents = do
liftIO $ withSystemTempFile "tempfile.yaml" $ \filePath fileHandle -> do
hClose fileHandle
BS.writeFile filePath $ Y.encodePretty Y.defConfig contents
editor <- do
lookupEnv "EDITOR" >>= \case
Just editor' -> return editor'
Nothing -> return "vim"
code <- system (editor <> " " <> filePath)
case code of
ExitSuccess -> do
newContents <- Y.decodeFileEither @Contents filePath
case newContents of
Right newContents' -> return $ Just newContents'
Left err -> do
print err
return Nothing
ExitFailure _ -> return Nothing
runCmd' :: forall p. (PersistentBackend p) => Either (ParseErrorBundle Text Void) ReplCommand -> Maybe (Prompt (InputT IO) ()) -> Prompt (InputT IO) ()
runCmd' cmd ret = do
let repl = case ret of
Just ret' -> ret'
Nothing -> return ()
case cmd of
Left err -> do
liftIO $ print err
repl
Right Quit -> return ()
Right Clear -> do
clear @p
repl
Right ShowContents -> do
context <- getContext
showContents context.contextBody
repl
Right ShowUsage -> do
context <- getContext
liftIO $ do
print context.contextTotalTokens
repl
Right ShowRequest -> do
prev <- getContext
let req = toRequest prev.contextRequest (prev.contextHeader <> prev.contextBody <> prev.contextFooter)
liftIO $ do
BS.putStr $ BS.toStrict $ encodePretty req
T.putStrLn ""
repl
Right ShowContext -> do
prev <- getContext
liftIO $ do
putStrLn $ show prev
repl
Right ShowSession -> do
prev <- getContext
liftIO $ do
T.putStrLn $ prev.contextSessionName
repl
Right ListSessions -> do
liftIO $ do
list <- withDB @p $ \conn -> listSessions @p conn
forM_ list $ \sessionName' -> T.putStrLn sessionName'
repl
Right (CopySession from' to') -> do
liftIO $ do
withDB @p $ \conn -> do
mv <- load @p conn from'
case mv of
Just v -> do
_ <- save @p conn (v {contextSessionName = to'})
return ()
Nothing -> T.putStrLn $ "Failed to load " <> from'
repl
Right (DeleteSession session) -> do
withDB @p $ \conn -> deleteSession @p conn session
repl
Right (SwitchSession session) -> do
mv <- withDB @p $ \conn -> load @p conn session
case mv of
Just v -> do
(env :: PromptEnv) <- get
put $ env {context = v}
Nothing -> liftIO $ T.putStrLn $ "Failed to load " <> session
repl
Right (ReadImage imagePath) -> do
callWithImage @p imagePath >>= showContents
repl
Right Help -> do
liftIO $ do
putStrLn ":quit"
putStrLn ":clear"
putStrLn ":show contents"
putStrLn ":show usage"
putStrLn ":show request"
putStrLn ":show context"
putStrLn ":show session"
putStrLn ":list sessions"
putStrLn ":copy session <from> <to>"
putStrLn ":delete session <session name>"
putStrLn ":switch session <session name>"
putStrLn ":help"
repl
Right (UserInput input) -> do
callWithText @p input >>= showContents
repl
Right Edit -> do
-- Open a temporary file with the default editor of the system.
-- Then send it as user input.
editWithEditor >>= \case
Just input -> do
callWithText @p input >>= showContents
repl
Nothing -> do
liftIO $ putStrLn "Failed to open the editor."
repl
Right EditRequest -> do
-- Open a json file of request and edit it with the default editor of the system.
-- Then, read the file and parse it as a request.
-- Finally, update the context with the new request.
prev <- getContext
let req = toRequest prev.contextRequest (prev.contextHeader <> prev.contextBody <> prev.contextFooter)
editRequestWithEditor req >>= \case
Just req' -> do
let newContext = prev {contextRequest = req'}
setContext @p newContext
repl
Nothing -> do
liftIO $ putStrLn "Failed to open the editor."
repl
Right EditContents -> do
prev <- getContext
editContentsWithEditor prev.contextBody >>= \case
Just contents' -> do
let newContext = prev {contextBody = contents'}
setContext @p newContext
repl
Nothing -> do
liftIO $ putStrLn "Failed to open the editor."
repl
Right EditHeader -> do
prev <- getContext
editContentsWithEditor prev.contextHeader >>= \case
Just contents' -> do
let newContext = prev {contextHeader = contents'}
setContext @p newContext
Nothing -> do
liftIO $ putStrLn "Failed to open the editor."
repl
Right EditFooter -> do
prev <- getContext
editContentsWithEditor prev.contextFooter >>= \case
Just contents' -> do
let newContext = prev {contextFooter = contents'}
setContext @p newContext
repl
Nothing -> do
liftIO $ putStrLn "Failed to open the editor."
repl
Right (Repl sessionName) -> do
runRepl' @p
Right (ListKeys) -> do
liftIO $ do
list <- withDB @p $ \conn -> listKeys @p conn
forM_ list $ \(KeyName namespace keyName) -> T.putStrLn $ namespace <> ":" <> keyName
repl
Right (GetKey namespace keyName) -> do
namespace' <- case namespace of
Just v -> return v
Nothing -> getSessionName
mv <- withDB @p $ \conn -> getKey @p conn (KeyName namespace' keyName)
case mv of
Just v -> do
liftIO $ T.putStrLn v
Nothing -> do
liftIO $ T.putStrLn $ "Failed to get " <> keyName
repl
Right (SetKey namespace keyName keyValue) -> do
namespace' <- case namespace of
Just v -> return v
Nothing -> getSessionName
withDB @p $ \conn -> setKey @p conn (KeyName namespace' keyName) keyValue
repl
Right (DeleteKey namespace keyName) -> do
namespace' <- case namespace of
Just v -> return v
Nothing -> getSessionName
withDB @p $ \conn -> deleteKey @p conn (KeyName namespace' keyName)
repl
runRepl' :: forall p. (PersistentBackend p) => Prompt (InputT IO) ()
runRepl' = do
cmd <- getUserCommand @p
runCmd' @p cmd (Just (runRepl' @p))
runRepl :: forall p. (PersistentBackend p) => [ToolProxy] -> [CustomInstructionProxy] -> Text -> Louter.ChatRequest -> Contents -> IO ()
runRepl tools customs sessionName defaultReq contents = do
config <- readConfig
runInputT
( Settings
{ complete = completeFilename,
historyFile = Just "intelli-monad.history",
autoAddHistory = True
}
)
(runPrompt @p tools customs sessionName defaultReq (push @p contents >> runRepl' @p))