├── .gitignore ├── LICENSE ├── README.md ├── app ├── Common.hs ├── Ghci.hs ├── Legacy.hs └── Main.hs ├── default.nix ├── demo ├── demo.py └── demo.yml ├── flake.lock ├── flake.nix ├── iter.cabal ├── justfile └── shell.nix /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | .envrc 3 | demo/Demo 4 | *.hi 5 | *.o 6 | result* 7 | dist-* 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Valentin Reis 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iter 🔁 2 | 3 | A code iteration tool running on the [Groq API](https://console.groq.com). 4 | 5 | This is an UX experiment and demo for code iteration with 6 | [RHLF-based](https://en.wikipedia.org/wiki/Reinforcement_learning_from_human_feedback) 7 | LLMs. It takes the form of a 8 | [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) 9 | with free-form text that lets the user quickly iterate on diffs and pipe 10 | feedback (e.g. compilers and test suites) into the LLM before triggering 11 | [self-reflection](https://github.com/rxlqn/awesome-llm-self-reflection). 12 | 13 | [![Video demo](https://img.youtube.com/vi/eR855VNPjhk/0.jpg)](https://www.youtube.com/watch?v=eR855VNPjhk) 14 | 15 | ## usage 16 | 17 | * [Create an account and generate your API key](https://console.groq.com) from Groq! 18 | 19 | * Install [Nix](https://nixos.org/). 20 | 21 | * Install `iter` via: 22 | 23 | ``` 24 | nix profile install github:freuk/iter#iter 25 | ``` 26 | 27 | Or directly run the `iter` binary via `nix run github:freuk/iter#iter`. 28 | 29 | By default, `iter` uses `mixtral-8x7b-32768`, a 32k sequence length MoE 30 | of 7b parameter language models from [Mistral AI](https://mistral.ai/). 31 | Use `--config` (see `demos/` for examples) to change this choice to one of the other available models. 32 | 33 | ## development 34 | 35 | `nix-shell` will give you a development environment for `iter`. 36 | 37 | * `ghcid`: live GHC feedback 38 | * `just`: useful commands for interactive development 39 | -------------------------------------------------------------------------------- /app/Common.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveAnyClass #-} 2 | {-# LANGUAGE DeriveFunctor #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE DerivingStrategies #-} 5 | {-# LANGUAGE FlexibleInstances #-} 6 | {-# LANGUAGE LambdaCase #-} 7 | {-# LANGUAGE NamedFieldPuns #-} 8 | {-# LANGUAGE OverloadedStrings #-} 9 | {-# LANGUAGE RecordWildCards #-} 10 | {-# LANGUAGE TupleSections #-} 11 | {-# LANGUAGE NoImplicitPrelude #-} 12 | 13 | module Common where 14 | 15 | import Control.Arrow ((>>>)) 16 | import Data.Aeson as Aeson (FromJSON, ToJSON) 17 | import qualified Data.Aeson as Aeson 18 | ( Options, 19 | ToJSON (toEncoding, toJSON), 20 | defaultOptions, 21 | fieldLabelModifier, 22 | genericParseJSON, 23 | genericToEncoding, 24 | genericToJSON, 25 | parseJSON, 26 | ) 27 | import qualified Data.List.NonEmpty as NE 28 | ( drop, 29 | head, 30 | length, 31 | ) 32 | import qualified Data.List.Split as Split (splitWhen) 33 | import qualified Data.Map as Map (empty) 34 | import qualified Data.Text as T (isPrefixOf, pack, unpack) 35 | import qualified Data.Time.Clock as Time (UTCTime) 36 | import qualified Data.Time.Clock.POSIX as Time (utcTimeToPOSIXSeconds) 37 | import qualified Data.Time.Format as Time (defaultTimeLocale, formatTime) 38 | import qualified Network.HTTP.Simple as HTTP 39 | ( Request, 40 | setRequestHeader, 41 | ) 42 | import Protolude 43 | import System.Console.Haskeline (InputT) 44 | import qualified System.Console.Haskeline as H (outputStrLn) 45 | import qualified Text.Casing as Casing (fromHumps, toKebab) 46 | import qualified Text.Printf as Printf (printf) 47 | 48 | data Opts token = Opts 49 | { optsIcDiffArgs :: Maybe [Text], 50 | optsFeedback :: Map Text [Text], 51 | optsLogLlm :: Maybe FilePath, 52 | optsModel :: Text, 53 | optsToken :: token, 54 | optsLegacyApi :: Maybe Bool 55 | } 56 | deriving stock (Show, Generic) 57 | 58 | instance FromJSON (Opts (Maybe Token)) where 59 | parseJSON = Aeson.genericParseJSON aesonOpts 60 | 61 | instance ToJSON (Opts (Maybe Token)) where 62 | toJSON = Aeson.genericToJSON aesonOpts 63 | toEncoding = Aeson.genericToEncoding aesonOpts 64 | 65 | type Token = Text 66 | 67 | type IterT a = 68 | InputT 69 | (StateT (Maybe (Iter (NonEmpty Text))) (ReaderT (Opts Token) IO)) 70 | a 71 | 72 | data Iter st = Iter 73 | { iterHistory :: st, 74 | iterContentPath :: FilePath, 75 | iterPerfLogs :: [Text] 76 | } 77 | deriving stock (Show, Functor) 78 | 79 | data PerformanceStats = PerformanceStats 80 | { timeGenerated :: Double, 81 | tokensGenerated :: Int, 82 | timeProcessed :: Double, 83 | tokensProcessed :: Int 84 | } 85 | deriving stock (Show, Generic) 86 | deriving anyclass (Aeson.FromJSON) 87 | 88 | outputText :: Text -> IterT () 89 | outputText = H.outputStrLn . T.unpack 90 | 91 | aesonOpts :: Aeson.Options 92 | aesonOpts = 93 | Aeson.defaultOptions 94 | { Aeson.fieldLabelModifier = Casing.toKebab . Casing.fromHumps . drop 4 95 | } 96 | 97 | defaultOpts :: Opts (Maybe Token) 98 | defaultOpts = 99 | Opts 100 | { optsIcDiffArgs = Just defaultOptsIcDiffArgs, 101 | optsFeedback = Map.empty, 102 | optsLogLlm = Nothing, 103 | optsModel = "mixtral-8x7b-32768", 104 | optsToken = Nothing, 105 | optsLegacyApi = Just defaultOptsLegacyApi 106 | } 107 | 108 | defaultOptsIcDiffArgs :: [Text] 109 | defaultOptsIcDiffArgs = ["-H", "--label=CURRENT", "--label=NEW", "--cols=160"] 110 | 111 | defaultOptsLegacyApi :: Bool 112 | defaultOptsLegacyApi = False 113 | 114 | withTokenAndDefaults :: Opts (Maybe Token) -> Token -> Opts Token 115 | withTokenAndDefaults Opts {..} token = 116 | Opts 117 | { optsIcDiffArgs = Just $ fromMaybe defaultOptsIcDiffArgs optsIcDiffArgs, 118 | optsFeedback, 119 | optsLogLlm, 120 | optsModel, 121 | optsToken = token, 122 | optsLegacyApi = Just $ fromMaybe defaultOptsLegacyApi optsLegacyApi 123 | } 124 | 125 | mapOrComplainAndReturnOld :: (Text -> IterT t) -> IterT (Maybe (Text, t)) 126 | mapOrComplainAndReturnOld f = 127 | lift get >>= \case 128 | Nothing -> outputText "no content yet" >> pure Nothing 129 | Just (Iter {iterHistory = old :| _}) -> Just . (old,) <$> f old 130 | 131 | whenJust :: (Applicative f) => Maybe a -> (a -> f ()) -> f () 132 | whenJust ma fb = maybe pass fb ma 133 | 134 | getHistoryHead :: IterT (Maybe Text) 135 | getHistoryHead = 136 | lift get <&> \case 137 | Nothing -> Nothing 138 | Just Iter {iterHistory} -> Just (NE.head iterHistory) 139 | 140 | getVersion :: IterT Int 141 | getVersion = 142 | lift get <&> \case 143 | Nothing -> 0 144 | Just Iter {iterHistory} -> NE.length iterHistory 145 | 146 | getFilePath :: IterT (Maybe FilePath) 147 | getFilePath = 148 | lift get <&> \case 149 | Nothing -> Nothing 150 | Just Iter {iterContentPath} -> Just iterContentPath 151 | 152 | getPrevious :: Int -> IterT (Maybe Text) 153 | getPrevious n = 154 | lift get <&> \case 155 | Nothing -> Nothing 156 | Just Iter {iterHistory} -> case NE.drop n iterHistory of 157 | [] -> Nothing 158 | content : _ -> Just content 159 | 160 | dumpPerformanceLogs :: Maybe Text -> IterT () 161 | dumpPerformanceLogs mText = 162 | lift get >>= \mIter -> whenJust mIter $ \x@Iter {iterPerfLogs} -> do 163 | whenJust mText outputText 164 | for_ iterPerfLogs outputText 165 | lift $ put $ Just x {iterPerfLogs = []} 166 | 167 | extractBlock :: Text -> Maybe (Text, Text, Text) 168 | extractBlock = 169 | (Split.splitWhen ("```" `T.isPrefixOf`) . lines) >>> \case 170 | preamble : block : rest -> 171 | Just 172 | ( unlines preamble, 173 | unlines block, 174 | unlines (mconcat rest) 175 | ) 176 | _ -> Nothing 177 | 178 | error :: Text -> IterT () 179 | error str = outputText ("error: " <> str) 180 | 181 | outputStrLnCurrent :: Text -> IterT () 182 | outputStrLnCurrent str = do 183 | outputText "" 184 | outputText . yellow $ indentWith " " str 185 | 186 | yellow :: Text -> Text 187 | yellow str = "\ESC[93m" <> str <> "\ESC[0m" 188 | 189 | blue :: Text -> Text 190 | blue str = "\ESC[34m" <> str <> "\ESC[0m" 191 | 192 | indentWith :: Text -> Text -> Text 193 | indentWith c = unlines . map (c <>) . lines 194 | 195 | setBearer :: Text -> HTTP.Request -> HTTP.Request 196 | setBearer token = 197 | HTTP.setRequestHeader "Authorization" ["Bearer " <> encodeUtf8 token] 198 | 199 | showPerf :: Text -> Time.UTCTime -> Time.UTCTime -> PerformanceStats -> Text 200 | showPerf model t0 t1 PerformanceStats {timeGenerated, tokensGenerated, timeProcessed} = 201 | T.pack $ 202 | Printf.printf 203 | "%0.3fs Tokens/s | %s | total: %ss queued for %ss input: %0.3fs gen: %0.3fs" 204 | (fromInteger (toInteger tokensGenerated) / timeGenerated) 205 | model 206 | (showDiffTime requestTime) 207 | (showDiffTime queueTime) 208 | timeProcessed 209 | timeGenerated 210 | where 211 | showDiffTime = Time.formatTime Time.defaultTimeLocale "%3Es" 212 | queueTime = requestTime - realToFrac timeGenerated - realToFrac timeProcessed 213 | requestTime = Time.utcTimeToPOSIXSeconds t1 - Time.utcTimeToPOSIXSeconds t0 214 | 215 | pushPerf :: [Text] -> IterT () 216 | pushPerf perfLogs = 217 | lift $ modify $ \case 218 | Nothing -> Nothing 219 | Just s@Iter {iterPerfLogs} -> 220 | Just $ s {iterPerfLogs = iterPerfLogs <> perfLogs} 221 | -------------------------------------------------------------------------------- /app/Ghci.hs: -------------------------------------------------------------------------------- 1 | module Ghci (demo) where 2 | 3 | import qualified Data.Yaml as Yaml 4 | import Main (Opts (..), defaultOpts, iterWithOpts) 5 | import Protolude 6 | 7 | demo :: IO () 8 | demo = do 9 | opts <- Yaml.decodeFileThrow "demo/demo.yml" 10 | iterWithOpts opts "demo/demo.py" 11 | 12 | self :: IO () 13 | self = do 14 | opts <- Yaml.decodeFileThrow "demo/hs.yml" 15 | iterWithOpts opts "app/Main.hs" 16 | -------------------------------------------------------------------------------- /app/Legacy.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveAnyClass #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE DerivingStrategies #-} 4 | {-# LANGUAGE NamedFieldPuns #-} 5 | {-# LANGUAGE OverloadedStrings #-} 6 | {-# LANGUAGE TypeApplications #-} 7 | {-# LANGUAGE ViewPatterns #-} 8 | {-# LANGUAGE NoImplicitPrelude #-} 9 | 10 | module Legacy (llmLegacy) where 11 | 12 | import Common 13 | import Data.Aeson ((.=)) 14 | import Data.Aeson as Aeson (FromJSON, ToJSON) 15 | import qualified Data.Aeson as Aeson (decode, object) 16 | import qualified Data.ByteString.Lazy.Char8 as C8 (lines) 17 | import qualified Data.Time.Clock.POSIX as Time (getCurrentTime) 18 | import qualified Network.HTTP.Simple as HTTP 19 | ( getResponseBody, 20 | httpLBS, 21 | setRequestBodyJSON, 22 | ) 23 | import Protolude 24 | 25 | data HistoryMessage = HistoryMessage 26 | { userPrompt :: Text, 27 | assistantResponse :: Text 28 | } 29 | deriving stock (Show, Generic) 30 | deriving anyclass (Aeson.ToJSON) 31 | 32 | newtype Result a = Result {result :: a} 33 | deriving stock (Show, Generic) 34 | deriving anyclass (Aeson.FromJSON) 35 | 36 | newtype Content = Content {content :: Text} 37 | deriving stock (Show, Generic) 38 | deriving anyclass (Aeson.FromJSON) 39 | 40 | newtype Stats = Stats {stats :: PerformanceStats} 41 | deriving stock (Show, Generic) 42 | deriving anyclass (Aeson.FromJSON) 43 | 44 | -- | same as llmOpenAiApi, but wants a legacy request token, and offers 45 | -- performance information. 46 | llmLegacy :: [(Text, Text)] -> [Text] -> [Text] -> IterT Text 47 | llmLegacy historyMessages (unlines -> sysPrompt) (unlines -> userPrompt) = do 48 | request <- do 49 | body <- do 50 | model <- optsModel <$> lift ask 51 | pure 52 | [ "model_id" .= model, 53 | "system_prompt" .= sysPrompt, 54 | "user_prompt" .= userPrompt, 55 | "history" .= mkHistorymessages historyMessages 56 | ] 57 | token <- optsToken <$> lift ask 58 | pure 59 | . setBearer token 60 | $ HTTP.setRequestBodyJSON 61 | (Aeson.object body) 62 | "POST https://api.groq.com/v1/request_manager/text_completion" 63 | 64 | t0 <- liftIO Time.getCurrentTime 65 | resp <- HTTP.getResponseBody <$> HTTP.httpLBS request 66 | t1 <- liftIO Time.getCurrentTime 67 | model <- optsModel <$> lift ask 68 | 69 | let results = 70 | catMaybes <$> fmap (Aeson.decode @(Result Content)) . C8.lines $ resp 71 | perf = 72 | catMaybes <$> fmap (Aeson.decode @(Result Stats)) . C8.lines $ resp 73 | 74 | perfLogs = maybe [] ((: []) . showPerf model t0 t1 . stats . result) (head perf) 75 | 76 | final = mconcat (content . result <$> results) 77 | 78 | logLlmCalls logFile = for_ 79 | [ ("s", yellow sysPrompt), 80 | ("u", blue userPrompt), 81 | ("r", final) 82 | ] 83 | $ \(prefix, content) -> 84 | appendFile logFile $ indentWith (prefix <> ":") $ content <> "\n" 85 | 86 | lift ask >>= maybe pass (liftIO . logLlmCalls) . optsLogLlm 87 | 88 | pushPerf perfLogs 89 | 90 | pure final 91 | 92 | mkHistorymessages :: [(Text, Text)] -> [HistoryMessage] 93 | mkHistorymessages messages = 94 | [ HistoryMessage {userPrompt, assistantResponse} 95 | | (userPrompt, assistantResponse) <- messages 96 | ] 97 | -------------------------------------------------------------------------------- /app/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ApplicativeDo #-} 2 | {-# LANGUAGE BlockArguments #-} 3 | {-# LANGUAGE DeriveAnyClass #-} 4 | {-# LANGUAGE DeriveGeneric #-} 5 | {-# LANGUAGE DerivingStrategies #-} 6 | {-# LANGUAGE LambdaCase #-} 7 | {-# LANGUAGE NamedFieldPuns #-} 8 | {-# LANGUAGE OverloadedStrings #-} 9 | {-# LANGUAGE TypeApplications #-} 10 | {-# LANGUAGE ViewPatterns #-} 11 | {-# LANGUAGE NoImplicitPrelude #-} 12 | 13 | module Main (main, iterWithOpts, Opts (..), defaultOpts) where 14 | 15 | import Common 16 | import Data.Aeson ((.=)) 17 | import Data.Aeson as Aeson (FromJSON, ToJSON) 18 | import qualified Data.Aeson as Aeson (decode, object) 19 | import qualified Data.Algorithm.Diff as Diff (getGroupedDiff) 20 | import qualified Data.Algorithm.DiffOutput as Diff 21 | ( diffToLineRanges, 22 | prettyDiffs, 23 | ) 24 | import qualified Data.List as List (elem, words) 25 | import qualified Data.List.NonEmpty as NE 26 | ( cons, 27 | head, 28 | nonEmpty, 29 | tail, 30 | ) 31 | import qualified Data.Map as Map (assocs, lookup) 32 | import Data.String (String) 33 | import qualified Data.Text as T (pack, replace, unpack) 34 | import qualified Data.Time.Clock.POSIX as Time (getCurrentTime) 35 | import qualified Data.Yaml as Yaml (decodeFileThrow) 36 | import Legacy 37 | import qualified Network.HTTP.Simple as HTTP 38 | ( getResponseBody, 39 | httpLBS, 40 | setRequestBodyJSON, 41 | ) 42 | import qualified Options.Applicative as OA 43 | ( Parser, 44 | ParserInfo, 45 | execParser, 46 | fullDesc, 47 | header, 48 | helper, 49 | info, 50 | long, 51 | metavar, 52 | optional, 53 | strArgument, 54 | strOption, 55 | ) 56 | import Protolude 57 | import qualified System.Console.Haskeline as H 58 | ( defaultSettings, 59 | getInputLine, 60 | handleInterrupt, 61 | runInputT, 62 | withInterrupt, 63 | ) 64 | import qualified System.Environment as Environment (lookupEnv) 65 | import qualified System.FilePath as FilePath (dropExtension, takeFileName) 66 | import System.IO (BufferMode (NoBuffering)) 67 | import qualified System.IO as IO 68 | ( hClose, 69 | hFlush, 70 | hPutStr, 71 | hSetBinaryMode, 72 | hSetBuffering, 73 | ) 74 | import qualified System.IO.Temp as IO (withSystemTempFile) 75 | import System.Process (ProcessHandle) 76 | import qualified System.Process as Process 77 | ( readProcessWithExitCode, 78 | spawnProcess, 79 | waitForProcess, 80 | ) 81 | 82 | readProcessWithExitCode :: Text -> [Text] -> Text -> IO (ExitCode, Text, Text) 83 | readProcessWithExitCode 84 | (T.unpack -> exe) 85 | (fmap T.unpack -> args) 86 | (T.unpack -> stdIn) = do 87 | (ec, out, err) <- Process.readProcessWithExitCode exe args stdIn 88 | return (ec, T.pack out, T.pack err) 89 | 90 | spawnProcess :: Text -> [Text] -> IO ProcessHandle 91 | spawnProcess (T.unpack -> cmd) (fmap T.unpack -> args) = 92 | Process.spawnProcess cmd args 93 | 94 | parserInfo :: OA.ParserInfo (FilePath, Maybe FilePath) 95 | parserInfo = 96 | OA.info (optionParser <**> OA.helper) $ 97 | OA.fullDesc 98 | <> OA.header "llm-based iteration tool." 99 | 100 | optionParser :: OA.Parser (FilePath, Maybe FilePath) 101 | optionParser = do 102 | cfg <- OA.optional $ OA.strOption $ OA.long "cfg" <> OA.metavar "" 103 | content <- OA.strArgument $ OA.metavar "" 104 | return (content, cfg) 105 | 106 | undo :: IterT Text 107 | undo = 108 | lift get >>= \case 109 | Nothing -> pure "" 110 | Just s@Iter {iterHistory = iterHistory@(lastProposal :| _)} -> 111 | case NE.nonEmpty (NE.tail iterHistory) of 112 | Nothing -> pure "" 113 | Just historyTail -> do 114 | lift $ put $ Just s {iterHistory = historyTail} 115 | return lastProposal 116 | 117 | main :: IO () 118 | main = 119 | OA.execParser parserInfo >>= \(mFile, mConfig) -> case mConfig of 120 | Nothing -> iter mFile 121 | Just cfgPath -> do 122 | opts <- Yaml.decodeFileThrow cfgPath 123 | iterWithOpts opts mFile 124 | 125 | iter :: FilePath -> IO () 126 | iter = iterWithOpts defaultOpts 127 | 128 | iterWithOpts :: Opts (Maybe Token) -> FilePath -> IO () 129 | iterWithOpts opts path = do 130 | liftIO (Environment.lookupEnv "GROQ_API_KEY") >>= \case 131 | Nothing -> case optsToken opts of 132 | Nothing -> 133 | (putText . unlines) 134 | [ "GROQ_API_KEY not set. Run:", 135 | "", 136 | " export HISTIGNORE=$HISTIGNORE':*GROQ_API_KEY*' # optional, bash only", 137 | " export GROQ_API_KEY=", 138 | "", 139 | "No key yet? Create an account and generate yours: https://console.groq.com" 140 | ] 141 | Just token -> go token 142 | Just token -> go (T.pack token) 143 | where 144 | go token = 145 | void 146 | . deepseq token 147 | $ runReaderT 148 | (runStateT (H.runInputT H.defaultSettings start) Nothing) 149 | (opts `withTokenAndDefaults` token) 150 | start = open path >> loop 151 | 152 | runEditor :: 153 | Text -> 154 | Text -> 155 | Text -> 156 | IO Text 157 | runEditor editorName templ initialContents = 158 | IO.withSystemTempFile 159 | (T.unpack templ) 160 | ( \iterContentPath hdl -> do 161 | IO.hSetBinaryMode hdl True 162 | IO.hSetBuffering hdl NoBuffering 163 | IO.hPutStr hdl (T.unpack initialContents) 164 | IO.hClose hdl 165 | prc <- spawnProcess editorName [T.pack iterContentPath] 166 | void $ Process.waitForProcess prc 167 | readFile iterContentPath 168 | ) 169 | 170 | proposeMore :: Text -> Int -> Text -> IterT () 171 | proposeMore instructions i new = do 172 | Just old <- getPrevious i 173 | opts <- lift ask 174 | newContent <- outputCurrentDiffAndReturnBlock opts new old 175 | dumpPerformanceLogs (Just "") 176 | lift $ modify ((fmap . fmap) (NE.cons newContent)) 177 | propose instructions (i + 1) 178 | 179 | outputCurrentDiffAndReturnBlock :: Opts a -> Text -> Text -> IterT Text 180 | outputCurrentDiffAndReturnBlock opts new old = case extractBlock new of 181 | Nothing -> do 182 | outputDiff opts old new 183 | pure new 184 | Just (before, block, after) -> do 185 | outputText $ blue before 186 | outputDiff opts old block 187 | outputText $ blue after 188 | pure block 189 | 190 | propose :: Text -> Int -> IterT () 191 | propose _ 0 = pass 192 | propose instructions numIters = do 193 | iterContentPath <- getFilePath 194 | Just new <- getHistoryHead 195 | currentVersion <- getVersion 196 | Just old <- getPrevious numIters 197 | mFilePath <- getFilePath 198 | opts <- lift ask 199 | 200 | minput <- 201 | H.getInputLine $ 202 | mconcat 203 | [ "[", 204 | show currentVersion, 205 | " -> ", 206 | show (currentVersion + 1), 207 | "] accept/a discard/d edit/e reflexion/r block/b undo/u xplain/x > " 208 | ] 209 | whenJust minput $ \raw -> case NE.nonEmpty (List.words raw) of 210 | Nothing -> do 211 | void $ outputCurrentDiffAndReturnBlock opts new old 212 | propose instructions numIters 213 | Just (inp :| []) 214 | | inp `List.elem` ["accept", "a"] -> do 215 | maybe pass (write new) iterContentPath 216 | outputStrLnCurrent new 217 | | inp `List.elem` ["discard", "d"] -> do 218 | replicateM_ numIters undo 219 | void $ mapOrComplainAndReturnOld outputStrLnCurrent 220 | | inp `List.elem` ["xplain", "x"] -> do 221 | outputText "" 222 | explainDiff old new >>= outputText . blue 223 | dumpPerformanceLogs (Just "") 224 | propose instructions numIters 225 | | inp `List.elem` ["edit", "e"] -> 226 | liftIO (Environment.lookupEnv "EDITOR") >>= \case 227 | Nothing -> do 228 | outputText "EDITOR environment variable not set" 229 | Just (T.pack -> editor) -> 230 | liftIO 231 | ( runEditor 232 | editor 233 | ( maybe 234 | "code.txt" 235 | (T.pack . FilePath.takeFileName) 236 | mFilePath 237 | ) 238 | new 239 | ) 240 | >>= proposeMore instructions numIters 241 | | inp `List.elem` ["reflexion", "r"] -> 242 | thinkMore instructions old new >>= proposeMore instructions numIters 243 | | inp `List.elem` ["undo", "u"] -> do 244 | undo 245 | void $ outputCurrentDiffAndReturnBlock opts new old 246 | propose instructions (numIters - 1) 247 | Just _ -> oneProposalStep instructions (T.pack raw) old new >>= proposeMore instructions numIters 248 | 249 | loop :: IterT () 250 | loop = H.handleInterrupt loop $ H.withInterrupt $ do 251 | version <- getVersion 252 | lift get >>= \case 253 | Nothing -> do 254 | minput <- H.getInputLine "new/n , open/o , quit/q > " 255 | whenJust minput $ \(List.words -> args) -> case args of 256 | ("q" : _) -> quit 257 | ("quit" : _) -> quit 258 | ["o", iterContentPath] -> open iterContentPath 259 | ["open", iterContentPath] -> open iterContentPath 260 | ["n", iterContentPath] -> create iterContentPath >> loop 261 | ["new", iterContentPath] -> create iterContentPath >> loop 262 | ("n" : _) -> error "new/n takes exactly one argument" >> loop 263 | ("new" : _) -> error "new/n takes exactly one argument" >> loop 264 | ("o" : _) -> error "open/o takes exactly one argument" >> loop 265 | ("open" : _) -> error "open/o takes exactly one argument" >> loop 266 | ((':' : _) : _) -> outputText "not a :command" >> loop 267 | [] -> loop 268 | something -> intend (unwords $ T.pack <$> something) >> loop 269 | Just Iter {iterHistory, iterContentPath} -> do 270 | minput <- 271 | H.getInputLine 272 | . mconcat 273 | $ [ "[", 274 | show version, 275 | "] :quit/q, :reload/r :write/w, :explain/x :discuss/d :feedback/f ", 276 | ":undo/u > " 277 | ] 278 | whenJust minput $ \(List.words -> args) -> case args of 279 | (":q" : _) -> quit 280 | (":quit" : _) -> quit 281 | (":r" : _) -> reload >> loop 282 | (":reload" : _) -> reload >> loop 283 | [":w"] -> write (NE.head iterHistory) iterContentPath >> loop 284 | [":write"] -> write (NE.head iterHistory) iterContentPath >> loop 285 | (":w" : _) -> error ":write/:w takes no argument" >> loop 286 | (":write" : _) -> error ":write/:w takes no argument" >> loop 287 | [":explain"] -> explain >> loop 288 | [":x"] -> explain >> loop 289 | [":discuss"] -> error ":discuss/d needs to be followed by a prompt" >> loop 290 | [":d"] -> error ":discuss/d needs to be followed by a prompt" >> loop 291 | (":discuss" : xs) -> discuss (unwords $ T.pack <$> xs) >> loop 292 | (":d" : xs) -> discuss (unwords $ T.pack <$> xs) >> loop 293 | (":feedback" : feedbackName : _) -> 294 | feedback iterContentPath (T.pack feedbackName) >> loop 295 | (":f" : feedbackName : _) -> 296 | feedback iterContentPath (T.pack feedbackName) >> loop 297 | (":feedback" : _) -> listFeedbacks iterContentPath >> loop 298 | (":f" : _) -> listFeedbacks iterContentPath >> loop 299 | (":undo" : _) -> undo >> loop 300 | (":u" : _) -> undo >> loop 301 | ((':' : _) : _) -> outputText "not a :command" >> loop 302 | [] -> mapOrComplainAndReturnOld outputStrLnCurrent >> loop 303 | prompt -> intend (unwords $ T.pack <$> prompt) >> loop 304 | 305 | listFeedbacks :: FilePath -> IterT () 306 | listFeedbacks iterContentPath = do 307 | outputText "\nusage: :feedback , where is one of:\n" 308 | feedbacks <- optsFeedback <$> lift ask 309 | for_ (Map.assocs feedbacks) $ \(i, fdb) -> case fdb of 310 | (cmd : (fmap (template iterContentPath) -> args)) -> 311 | outputText $ i <> ": " <> cmd <> " " <> unwords args 312 | _ -> pass 313 | outputText "" 314 | 315 | feedback :: FilePath -> Text -> IterT () 316 | feedback iterContentPath name = do 317 | feedbacks <- optsFeedback <$> lift ask 318 | case Map.lookup name feedbacks of 319 | Just fdb@(cmd : (fmap (template iterContentPath) -> args)) -> 320 | liftIO (readProcessWithExitCode cmd args "") >>= \case 321 | (ec, out, err) -> do 322 | for_ 323 | ["Feedback from " <> unwords ([cmd] <> args), out, err] 324 | outputText 325 | case ec of 326 | ExitSuccess -> outputText "Feedback successful." 327 | _ -> do 328 | outputText "Feedback failed. Applying feedback.." 329 | intend 330 | . unlines 331 | $ [ "Apply the following feedback from the `" 332 | <> unwords fdb 333 | <> "` command to this code:\n", 334 | "```", 335 | out, 336 | err, 337 | "```" 338 | ] 339 | _ -> outputText "No such feedback" 340 | 341 | template :: FilePath -> Text -> Text 342 | template iterContentPath = templateFile . templateBasename 343 | where 344 | baseName = T.pack . FilePath.dropExtension $ FilePath.takeFileName iterContentPath 345 | fileName = T.pack iterContentPath 346 | templateFile = T.replace "{file}" fileName 347 | templateBasename = T.replace "{basename}" baseName 348 | 349 | quit :: IterT () 350 | quit = return () 351 | 352 | open :: FilePath -> IterT () 353 | open iterContentPath = do 354 | content <- liftIO $ readFile iterContentPath 355 | lift 356 | . put 357 | $ Just 358 | Iter 359 | { iterHistory = pure content, 360 | iterContentPath = iterContentPath, 361 | iterPerfLogs = [] 362 | } 363 | void $ mapOrComplainAndReturnOld outputStrLnCurrent 364 | 365 | write :: Text -> FilePath -> IterT () 366 | write content fn = do 367 | liftIO $ writeFile fn content 368 | outputText $ "successfully written to " <> T.pack fn 369 | 370 | reload :: IterT () 371 | reload = 372 | getFilePath >>= \case 373 | Nothing -> pure () 374 | Just fp -> do 375 | content <- liftIO $ readFile fp 376 | lift 377 | . put 378 | $ Just 379 | Iter 380 | { iterHistory = pure content, 381 | iterContentPath = fp, 382 | iterPerfLogs = [] 383 | } 384 | outputText $ "successfully reloaded " <> T.pack fp 385 | outputStrLnCurrent content 386 | 387 | create :: FilePath -> IterT () 388 | create iterContentPath = do 389 | liftIO $ writeFile iterContentPath "" 390 | lift $ put (Just Iter {iterHistory = pure "", iterContentPath, iterPerfLogs = []}) 391 | 392 | intend :: Text -> IterT () 393 | intend instructions = do 394 | H.withInterrupt (H.handleInterrupt cancelIntention go) >>= \case 395 | Nothing -> pure () 396 | Just (old, new) 397 | | old == new -> outputText "WARNING: no changes" >> outputText "" 398 | | otherwise -> proposeMore instructions 0 new 399 | where 400 | go = mapOrComplainAndReturnOld (oneInstructionStep instructions) 401 | cancelIntention = outputText "Cancelled" >> pure Nothing 402 | 403 | explain :: IterT () 404 | explain = discuss "Explain what this program does." 405 | 406 | discuss :: Text -> IterT () 407 | discuss prompt = do 408 | getHistoryHead >>= \case 409 | Nothing -> return () 410 | Just code -> H.handleInterrupt (outputText "Cancelled.") $ 411 | H.withInterrupt $ 412 | do 413 | outputText "" 414 | oneDiscussionStep prompt code >>= (outputText . blue) 415 | dumpPerformanceLogs (Just "") 416 | 417 | styleGuidelines :: [Text] 418 | styleGuidelines = 419 | [ "Reply in markdown.", 420 | "Keep all the source code in a single file, don't split things apart in multiple files.", 421 | "don't add other files" 422 | ] 423 | 424 | oneProposalStep :: Text -> Text -> Text -> Text -> IterT Text 425 | oneProposalStep goal instructions old new = do 426 | mFilePath <- getFilePath 427 | llm [] (sysPrompt mFilePath) userPrompt 428 | where 429 | userPrompt = 430 | [ "Our overall task is:" <> goal, 431 | "Here's the adjustment I am requesting to our diff now:" <> instructions 432 | ] 433 | 434 | sysPrompt mFilePath = 435 | [ "We are working together on the code block below:", 436 | "```", 437 | old, 438 | "```", 439 | "Here is our diff so far:", 440 | "```", 441 | diffString old new, 442 | "```", 443 | "Which means our new version of the code is:", 444 | "```", 445 | new, 446 | "```", 447 | "When asked for a change, return ONLY YOUR NEW VERSION OF THE CODE.", 448 | "Do NOT say anything else." 449 | ] 450 | <> styleGuidelines 451 | <> btwFileNameIs mFilePath 452 | 453 | oneInstructionStep :: Text -> Text -> IterT Text 454 | oneInstructionStep instructions code = do 455 | mFilePath <- getFilePath 456 | llm [] (sysPrompt mFilePath) userPrompt 457 | where 458 | userPrompt = 459 | [ "Here's the current version of the source:", 460 | "```", 461 | code, 462 | "```", 463 | "And here's the task we're working on:", 464 | instructions, 465 | "Your reply should include a SINGLE code block. Only one!" 466 | ] 467 | 468 | sysPrompt mFilePath = 469 | (if code /= "" then askForNewVersion else askForGenericHelp) 470 | <> styleGuidelines 471 | <> btwFileNameIs mFilePath 472 | where 473 | askForGenericHelp = ["You help people write software."] 474 | askForNewVersion = 475 | [ "You will write a new version of a source file.", 476 | "Return ONLY YOUR NEW VERSION OF THE CODE and nothing else", 477 | "Do NOT say anything else.", 478 | "" 479 | ] 480 | 481 | oneDiscussionStep :: Text -> Text -> IterT Text 482 | oneDiscussionStep prompt code = do 483 | mFilePath <- getFilePath 484 | llm [] (sysPrompt mFilePath) userPrompt 485 | where 486 | sysPrompt mFilePath = 487 | ["You will answer a question about a piece of code."] 488 | <> styleGuidelines 489 | <> btwFileNameIs mFilePath 490 | userPrompt = 491 | [ "The question is: " <> prompt, 492 | " It's about this code:", 493 | "```", 494 | code, 495 | "```", 496 | "Remember, the question is: " <> prompt, 497 | "Answer in TWO sentences and 50 words MAXIMUM." 498 | ] 499 | 500 | explainDiff :: Text -> Text -> IterT Text 501 | explainDiff = discussDiff "Your task is to describe these changes." 502 | 503 | discussDiff :: Text -> Text -> Text -> IterT Text 504 | discussDiff instructions old new = do 505 | mFilePath <- getFilePath 506 | llm [] (sysPrompt mFilePath) [instructions] 507 | where 508 | sysPrompt mFilePath = 509 | [ "We are working together on the code block below:", 510 | "```", 511 | old, 512 | "```", 513 | "Here is our diff so far:", 514 | "```", 515 | diffString old new, 516 | "```" 517 | ] 518 | <> styleGuidelines 519 | <> btwFileNameIs mFilePath 520 | 521 | btwFileNameIs :: Maybe FilePath -> [Text] 522 | btwFileNameIs = maybe [] (btw . T.pack) 523 | where 524 | btw iterContentPath = ["The file is named " <> iterContentPath <> ", in case that helps."] 525 | 526 | thinkMore :: Text -> Text -> Text -> IterT Text 527 | thinkMore instructions code new = do 528 | mFilePath <- getFilePath 529 | let go [] history = pure history 530 | go (prompt : remainingPrompts) history = do 531 | outputText $ "Self-reflection in progress: " <> blue prompt 532 | result <- llm history (sysPrompt mFilePath) [prompt] 533 | dumpPerformanceLogs Nothing 534 | go remainingPrompts ((prompt, result) : history) 535 | results <- go (mconcat <$> selfReflexionRecipe) [] 536 | pure $ maybe "" snd (head results) 537 | where 538 | -- My boss's [..] boss's boss's favourite self-reflexion prompt. 539 | selfReflexionRecipe = 540 | [ [ "Write a detailed outline to answer the following request in a", 541 | "non-obvious, creative and engaging manner:", 542 | instructions 543 | ], 544 | ["How could this outline be better?"], 545 | ["Apply that to the outline."], 546 | ["Now write a response to the request:", instructions], 547 | ["How could that answer be better?"], 548 | [ "Apply that to the answer. Return ONLY your new version of the", 549 | " original source code, and nothing else" 550 | ] 551 | ] 552 | 553 | sysPrompt mFilePath = 554 | [ "We are working together on changing code block below:", 555 | "```", 556 | code, 557 | "```", 558 | "Here the initial diff you've provided. Note: I'm not sure that is an improvement (maybe?).", 559 | "```", 560 | diffString code new, 561 | "```", 562 | "Our goal is to satisfy the following request:", 563 | instructions 564 | ] 565 | <> styleGuidelines 566 | <> btwFileNameIs mFilePath 567 | 568 | llm :: [(Text, Text)] -> [Text] -> [Text] -> IterT Text 569 | llm historyMessages sysPrompt userPrompt = 570 | do 571 | legacy <- lift $ fromMaybe defaultOptsLegacyApi . optsLegacyApi <$> ask 572 | let f = if legacy then llmLegacy else llmOpenAiApi 573 | f historyMessages sysPrompt userPrompt 574 | 575 | llmOpenAiApi :: [(Text, Text)] -> [Text] -> [Text] -> IterT Text 576 | llmOpenAiApi historyMessages (unlines -> sysPrompt) (unlines -> userPrompt) = do 577 | token <- optsToken <$> lift ask 578 | model <- optsModel <$> lift ask 579 | let body = 580 | [ "model" .= model, 581 | "messages" 582 | .= mconcat 583 | [ [Message {role = "system", Main.content = sysPrompt}], 584 | mconcat 585 | [ [ Message {role = "user", Main.content = prompt}, 586 | Message {role = "assistant", Main.content = reply} 587 | ] 588 | | (prompt, reply) <- historyMessages 589 | ], 590 | [Message {role = "user", Main.content = userPrompt}] 591 | ] 592 | ] 593 | request = 594 | setBearer token $ 595 | HTTP.setRequestBodyJSON 596 | (Aeson.object body) 597 | "POST https://api.groq.com/openai/v1/chat/completions" 598 | 599 | t0 <- liftIO Time.getCurrentTime 600 | resp <- HTTP.getResponseBody <$> HTTP.httpLBS request 601 | t1 <- liftIO Time.getCurrentTime 602 | 603 | case Aeson.decode @OpenAiResult resp of 604 | Just result@OpenAiResult {choices} -> case head choices of 605 | Just Choice {message} -> do 606 | let logLlmCalls logFile = for_ 607 | [ ("s", yellow sysPrompt), 608 | ("u", blue userPrompt), 609 | ("r", content message) 610 | ] 611 | $ \(prefix, content) -> 612 | appendFile logFile $ indentWith (prefix <> ":") $ content <> "\n" 613 | 614 | lift ask >>= maybe pass (liftIO . logLlmCalls) . optsLogLlm 615 | 616 | pushPerf 617 | [ showPerf 618 | model 619 | t0 620 | t1 621 | PerformanceStats 622 | { timeGenerated = completion_time $ usage result, 623 | tokensGenerated = completion_tokens $ usage result, 624 | timeProcessed = prompt_time $ usage result, 625 | tokensProcessed = prompt_tokens $ usage result 626 | } 627 | ] 628 | pure $ Main.content message 629 | Nothing -> pure "" 630 | Nothing -> pure "" 631 | 632 | data OpenAiResult = OpenAiResult 633 | { id :: Text, 634 | object :: Text, 635 | created :: Int, 636 | model :: Text, 637 | choices :: [Choice], 638 | usage :: Usage 639 | } 640 | deriving stock (Show, Generic) 641 | deriving anyclass (Aeson.FromJSON) 642 | 643 | data Usage = Usage 644 | { prompt_time :: Double, 645 | prompt_tokens :: Int, 646 | completion_time :: Double, 647 | completion_tokens :: Int, 648 | total_time :: Double, 649 | total_tokens :: Int 650 | } 651 | deriving stock (Show, Generic) 652 | deriving anyclass (Aeson.FromJSON, Aeson.ToJSON) 653 | 654 | data Choice = Choice 655 | { index :: Int, 656 | message :: Message, 657 | logprobs :: () 658 | } 659 | deriving stock (Show, Generic) 660 | deriving anyclass (Aeson.FromJSON, Aeson.ToJSON) 661 | 662 | data Message = Message {role :: Text, content :: Text} 663 | deriving stock (Show, Generic) 664 | deriving anyclass (Aeson.FromJSON, Aeson.ToJSON) 665 | 666 | outputDiff :: Opts a -> Text -> Text -> IterT () 667 | outputDiff Opts {optsIcDiffArgs} old new 668 | | old == new = outputText "" 669 | | otherwise = 670 | liftIO (wrapDiff "icdiff" (fromMaybe [] optsIcDiffArgs) old new) >>= \case 671 | (ExitSuccess, out, _) -> outputText out 672 | (ExitFailure i, _, _) -> 673 | outputText $ "(icdiff failed with exit code" <> show i <> ")" 674 | 675 | diffString :: Text -> Text -> Text 676 | diffString (unpackLines -> old) (unpackLines -> new) = 677 | show . Diff.prettyDiffs . Diff.diffToLineRanges $ Diff.getGroupedDiff old new 678 | 679 | unpackLines :: Text -> [String] 680 | unpackLines = fmap T.unpack . lines 681 | 682 | wrapDiff :: Text -> [Text] -> Text -> Text -> IO (ExitCode, Text, Text) 683 | wrapDiff cmd args old new = 684 | IO.withSystemTempFile "iter-old.txt" $ \fileOld hOld -> do 685 | void $ hPutStrLn hOld old 686 | IO.hFlush hOld 687 | IO.withSystemTempFile "iter-new.txt" $ \fileNew hNew -> do 688 | void $ hPutStrLn hNew new 689 | IO.hFlush hNew 690 | readProcessWithExitCode 691 | cmd 692 | ([T.pack fileOld, T.pack fileNew] <> args) 693 | "" 694 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | 3 | let 4 | 5 | inherit (builtins) filterSource baseNameOf elem; 6 | inherit (pkgs.lib) getBin; 7 | 8 | _iter = srcBasenames: 9 | let 10 | src = filterSource (path: _: elem (baseNameOf path) srcBasenames) ./.; 11 | iter = pkgs.haskellPackages.callCabal2nix "iter" src { }; 12 | in pkgs.haskell.lib.overrideCabal iter (old: { 13 | buildDepends = [ pkgs.makeWrapper ]; 14 | postInstall = 15 | "wrapProgram $out/bin/iter --prefix PATH : ${getBin pkgs.icdiff}/bin"; 16 | }); 17 | 18 | in rec { 19 | inherit pkgs; 20 | 21 | iter = 22 | _iter [ "iter.cabal" "LICENSE" "app" "Main.hs" "Legacy.hs" "Common.hs" ]; 23 | 24 | shell = pkgs.haskellPackages.shellFor { 25 | # only include "iter.cabal" for faster iteration via 'cached-nix-shell' 26 | packages = p: [ (_iter [ "iter.cabal" ]) ]; 27 | buildInputs = [ 28 | pkgs.icdiff # pkgs.haskellPackages.shellFor does not pick this up 29 | pkgs.ghcid 30 | pkgs.ormolu 31 | pkgs.haskellPackages.hlint 32 | pkgs.haskellPackages.apply-refact 33 | pkgs.cabal-install 34 | pkgs.nixfmt 35 | pkgs.ormolu 36 | pkgs.just 37 | pkgs.pylint 38 | pkgs.black 39 | pkgs.mypy 40 | ]; 41 | }; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /demo/demo.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | 4 | customers = [ 5 | {"name": "John Doe", "country": "USA" }, 6 | {"name": "Jane Smith", "country": "Canada"}, 7 | {"name": "Alice Johnson", "country": "UK"}, 8 | {"name": "Bob Brown", "country": "Australia"} 9 | ] 10 | 11 | def filter_customers_by_country(country): 12 | return [customer for customer in customers if customer["country"] == country] 13 | 14 | def load_customers_from_file(file_path): 15 | with open(file_path, 'r') as file: 16 | global customers 17 | customers = json.load(file) 18 | 19 | def main(country=None, file_path=None): 20 | if file_path: 21 | load_customers_from_file(file_path) 22 | if country: 23 | filtered_customers = filter_customers_by_country(country) 24 | else: 25 | filtered_customers = customers 26 | for customer in filtered_customers: 27 | print(customer) 28 | 29 | if __name__ == "__main__": 30 | parser = argparse.ArgumentParser() 31 | parser.add_argument("--country", help="Filter customers by country") 32 | parser.add_argument("--file", help="Path to the file containing customers") 33 | args = parser.parse_args() 34 | main(args.country, args.file) 35 | -------------------------------------------------------------------------------- /demo/demo.yml: -------------------------------------------------------------------------------- 1 | model: "mixtral-8x7b-32768" 2 | # model: "llama2-70b-4096" <- somewhat better at Haskell 3 | # model: "gemma-7b-it" <- doesn't follow the hardcoded prompts very well 4 | 5 | # these available templates get replaced in strings: {file}, {basename} 6 | feedback: 7 | # [, , , ..] 8 | execute: ["python", "{file}"] 9 | country: ["python", "{file}", "--country=USA"] 10 | pylint: ["pylint", "{file}"] 11 | 12 | # set to true if you have an old-style 'request_manager' token. 13 | legacy-api: false 14 | 15 | # define this if you'd like to log the full LLM interaction sequence to a 16 | # file. 17 | # log-llm: "/tmp/iter.log" 18 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1705309234, 9 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "id": "flake-utils", 17 | "type": "indirect" 18 | } 19 | }, 20 | "nixpkgs": { 21 | "locked": { 22 | "lastModified": 1708815994, 23 | "narHash": "sha256-hL7N/ut2Xu0NaDxDMsw2HagAjgDskToGiyZOWriiLYM=", 24 | "owner": "NixOS", 25 | "repo": "nixpkgs", 26 | "rev": "9a9dae8f6319600fa9aebde37f340975cab4b8c0", 27 | "type": "github" 28 | }, 29 | "original": { 30 | "id": "nixpkgs", 31 | "type": "indirect" 32 | } 33 | }, 34 | "root": { 35 | "inputs": { 36 | "flake-utils": "flake-utils", 37 | "nixpkgs": "nixpkgs" 38 | } 39 | }, 40 | "systems": { 41 | "locked": { 42 | "lastModified": 1681028828, 43 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 44 | "owner": "nix-systems", 45 | "repo": "default", 46 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "nix-systems", 51 | "repo": "default", 52 | "type": "github" 53 | } 54 | } 55 | }, 56 | "root": "root", 57 | "version": 7 58 | } 59 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "iter"; 3 | 4 | outputs = { self, nixpkgs, flake-utils }: 5 | flake-utils.lib.eachDefaultSystem (system: 6 | let 7 | 8 | packages = (import ./default.nix) { 9 | pkgs = import nixpkgs { 10 | system = system; 11 | config.allowBroken = true; 12 | }; 13 | }; 14 | in { 15 | inherit packages; 16 | devShells.default = packages.shell; 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /iter.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 3.0 2 | name: iter 3 | version: 0.1.0.0 4 | homepage: https://github.com/freuk/iter 5 | license: MIT 6 | license-file: LICENSE 7 | author: Valentin Reis 8 | maintainer: fre@freux.fr 9 | category: Development 10 | build-type: Simple 11 | extra-doc-files: CHANGELOG.md 12 | 13 | executable iter 14 | ghc-options: -Wall 15 | main-is: Main.hs 16 | other-modules: Legacy, Common 17 | hs-source-dirs: app 18 | default-language: Haskell2010 19 | build-depends: 20 | Diff, 21 | aeson, 22 | base, 23 | bytestring, 24 | casing, 25 | containers, 26 | filepath, 27 | haskeline, 28 | http-conduit, 29 | mtl, 30 | optparse-applicative, 31 | pretty-simple, 32 | process, 33 | protolude, 34 | split, 35 | temporary, 36 | text, 37 | time, 38 | yaml 39 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | _default: 2 | @just -l -u 3 | 4 | # start ghci with the Ghci module loaded 5 | ghci: 6 | ghci -iapp app/Ghci.hs 7 | 8 | # run ormolu and hlint 9 | lint: 10 | ormolu -i app/*.hs 11 | hlint app/*.hs 12 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | nixpkgs = builtins.fetchTarball 3 | "https://github.com/NixOS/nixpkgs/archive/refs/tags/23.11.tar.gz"; 4 | 5 | in { pkgs ? import ./default.nix { pkgs = import nixpkgs { }; } }: pkgs.shell 6 | --------------------------------------------------------------------------------