packages feed

ollama-holes-plugin (empty) → 0.1.0.0

raw patch · 5 files changed

+393/−0 lines, 5 filesdep +basedep +ghcdep +ollama-haskell

Dependencies added: base, ghc, ollama-haskell, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for OllamaHoles++## 0.1.0.0 -- 2024-04-25++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2025 Matthias Pall Gissurarson++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,75 @@+# Ollama Holes++![image](https://github.com/user-attachments/assets/649ffcd2-0560-47d6-bbbe-74bae08cbb70)++## Introduction+This is an example of a typed-hole plugin for GHC that uses the [Ollama](https://ollama.com/) to host a local LLM to fill in holes in Haskell code.+++## Example+Given ++```haskell+{-# OPTIONS_GHC -fplugin=GHC.Plugin.OllamaHoles #-}+{-# OPTIONS_GHC -fplugin-opt=GHC.Plugin.OllamaHoles:model=gemma3:27b #-}+{-# OPTIONS_GHC -fplugin-opt=GHC.Plugin.OllamaHoles:n=5 #-}++module Main where++import Data.List+++main :: IO ()+main = do let k = (_b :: [Int] -> [String])+          print (k [1,2,3])++```++We get the following output:+++```text+    Main.hs:12:20: error: [GHC-88464]+        • Found hole: _b :: [Int] -> [String]+        Or perhaps ‘_b’ is mis-spelled, or not in scope+        • In the expression: _b :: [Int] -> [String]+        In an equation for ‘k’: k = (_b :: [Int] -> [String])+        In the expression:+            do let k = (_b :: [Int] -> [String])+            print (k [1, 2, ....])+        • Relevant bindings include+            k :: [Int] -> [String] (bound at Main.hs:12:15)+            main :: IO () (bound at Main.hs:12:1)+        Valid hole fits include+            \x -> map show x+            map (show)+            \x -> map (\y -> "Number: " ++ show y) x+            \x -> replicate (length x) "Hello"+            []+    |+    12 | main = do let k = (_b :: [Int] -> [String])+    |                    ^^+    Error: cabal: Failed to build exe:main from OllamaHolesTest-1.0.0.+```++## Installation++1. Install [Ollama](https://ollama.com/download)+2. Install the `gemma3:27b` model (or any other model you prefer) using the following command:++```bash++ollama pull gemma3:27b+```+3. Clone this repository and navigate to the directory, and build the project using:++```bash+cabal build+```+4. Run the example using:++```bash+cabal build Test+```++5. Enjoy! If you want to change the underlying model, make sure to pass the model name via the plugin arguments (see example)
+ ollama-holes-plugin.cabal view
@@ -0,0 +1,115 @@+cabal-version:      3.0+-- The cabal-version field refers to the version of the .cabal specification,+-- and can be different from the cabal-install (the tool) version and the+-- Cabal (the library) version you are using. As such, the Cabal (the library)+-- version used must be equal or greater than the version stated in this field.+-- Starting from the specification version 2.2, the cabal-version field must be+-- the first thing in the cabal file.++-- Initial package description 'OllamaHoles' generated by+-- 'cabal init'. For further documentation, see:+--   http://haskell.org/cabal/users-guide/+--+-- The name of the package.+name:               ollama-holes-plugin++-- The package version.+-- See the Haskell package versioning policy (PVP) for standards+-- guiding when and how versions should be incremented.+-- https://pvp.haskell.org+-- PVP summary:     +-+------- breaking API changes+--                  | | +----- non-breaking API additions+--                  | | | +--- code changes with no API change+version:            0.1.0.0++-- A short (one-line) description of the package.+synopsis: A typed-hole plugin that uses LLMs via Ollama to generate valid hole-fits++-- A longer description of the package.+description: This package provides a GHC plugin that uses LLMs via Ollama to generate valid hole-fits.+             .+             The following flags are available:+             .+             To specify the model to use:+             .+             > -fplugin-opt=GHC.Plugin.OllamaHoles:model=<model_name>+             .+             To specify how many fits to generate (passed to the model)+             .+             > -fplugin-opt=GHC.Plugin.OllamaHoles:n=5+             .+             To enable debug output:+             .+             > -fplugin-opt=GHC.Plugin.OllamaHoles:debug=True+             .+             Before using this plugin, make sure you have the Ollama CLI installed and the model+             you want to use is available. You can install the Ollama CLI by following the+             instructions at [https://ollama.com/download](https://ollama.com/download),+             and you can install the default model (gemma3:27b) by running `ollama pull gemma3:27b`.+             .+             Note that the speed and quality of the hole-fits generated by the plugin depends on+             the model you use, and the default model requires a GPU to run efficiently.+             For a smaller model, we suggest `gemma3:4b-it-qat`, or `deepcoder:1.5b`.++++-- The license under which the package is released.+license:            MIT++-- The file containing the license text.+license-file:       LICENSE++-- The package author(s).+author:             Matthias Pall Gissurarson <mpg@mpg.is>++-- An email address to which users can send suggestions, bug reports, and patches.+maintainer:         Matthias Pall Gissurarson <mpg@mpg.is>+++-- A copyright notice.+copyright:          2025- Matthias Pall Gissurarson  +category:           Development, Compiler Plugin+tested-with:        GHC == 9.6.*+build-type:         Simple++-- Extra doc files to be distributed with the package, such as a CHANGELOG or a README.+extra-doc-files:    CHANGELOG.md+                    README.md++homepage:       https://github.com/Tritlo/OllamaHoles++-- Extra source files to be distributed with the package, such as examples, or a tutorial module.+-- extra-source-files:++source-repository head+  type:     git+  location: git://github.com/Tritlo/OllamaHoles.git++common warnings+    ghc-options: -Wall++library+    -- Import common warning flags.+    import:           warnings++    -- Modules exported by the library.+    exposed-modules:  GHC.Plugin.OllamaHoles++    -- Modules included in this library but not exported.+    -- other-modules:++    -- LANGUAGE extensions used by modules in this package.+    -- other-extensions:++    -- Other library packages from which modules are imported.+    build-depends:    base ^>=4.18.2.1,+                      ghc ^>=9.6,+                      ollama-haskell ^>= 0.1,+                      text ^>= 2.1,+++    -- Directories containing source files.+    hs-source-dirs:   src++    -- Base language which the package is written in.+    default-language: GHC2021
+ src/GHC/Plugin/OllamaHoles.hs view
@@ -0,0 +1,178 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- | The Ollama plugin for GHC+module GHC.Plugin.OllamaHoles (plugin) where++import Control.Monad (unless, when)+import Data.Char (isSpace)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.IO qualified as T+import GHC.Plugins hiding ((<>))+import GHC.Tc.Types+import GHC.Tc.Types.Constraint (Hole (..), ctLocEnv, ctLocSpan)+import GHC.Tc.Utils.Monad (getGblEnv, newTcRef)+import Ollama (GenerateOps (..))+import Ollama qualified++-- | Options for the LLM model+genOps :: Ollama.GenerateOps+genOps =+    Ollama.defaultGenerateOps+        { modelName = ""+        , prompt = ""+        }++promptTemplate :: Text+promptTemplate =+    "You are a typed-hole plugin within GHC, the Glasgow Haskell Compiler.\n"+        <> "You are given a hole in a Haskell program, and you need to fill it in.\n"+        <> "The hole is represented by the following information:\n"+        <> "{module}\n{location}\n{imports}\n{hole_var}\n{hole_type}\n{relevant_constraints}\n{local_env}\n{global_env}\n{candidate_fits}\n\n"+        <> "Provide one or more Haskell expressions that could fill this hole.\n"+        <> "This means coming up with an expression of the correct type that satisfies the constraints.\n"+        <> "Pay special attention to the type of the hole, specifically whether it is a function.\n"+        <> "Make sure you synthesize an expression that matches the type of the hole.\n"+        <> "Output ONLY the raw Haskell expression(s), one per line.\n"+        <> "Do not include explanations, introductions, or any surrounding text.\n"+        <> "Feel free to include any other functions from the list of imports to generate more complicated expressions.\n"+        <> "Output a maximum of {numexpr} expresssions.\n"++-- | Ollama plugin for GHC+plugin :: Plugin+plugin =+    defaultPlugin+        { holeFitPlugin = \opts ->+            Just $+                HoleFitPluginR+                    { hfPluginInit = newTcRef ()+                    , hfPluginStop = \_ -> return ()+                    , hfPluginRun =+                        const+                            HoleFitPlugin+                                { candPlugin = \_ c -> return c -- Don't filter candidates+                                , fitPlugin = \hole fits -> do+                                    let Flags{..} = parseFlags opts+                                    dflags <- getDynFlags+                                    gbl_env <- getGblEnv+                                    let mod_name = moduleNameString $ moduleName $ tcg_mod gbl_env+                                        imports = tcg_imports gbl_env+                                    liftIO $ do+                                        available_models <- Ollama.list+                                        case available_models of+                                            Nothing -> T.putStrLn "--- Ollama plugin: No models available.--"+                                            Just (Ollama.Models models) -> do+                                                unless (any ((== model_name) . Ollama.name) models) $+                                                    error $+                                                        "--- Ollama plugin: Model "+                                                            <> T.unpack model_name+                                                            <> " not found. "+                                                            <> "Use `ollama pull` to download the model, or specify another model using "+                                                            <> "`-fplugin-opt=GHC.Plugin.OllamaHoles:model=<model_name>` ---"+                                        when debug $ T.putStrLn "--- Ollama Plugin: Hole Found ---"+                                        let mn = "Module: " <> mod_name+                                        let lc = "Location: " <> showSDoc dflags (ppr $ ctLocSpan . hole_loc <$> th_hole hole)+                                        let im = "Imports: " <> showSDoc dflags (ppr $ moduleEnvKeys $ imp_mods imports)++                                        case th_hole hole of+                                            Just h -> do+                                                let lcl_env = ctLocEnv (hole_loc h)+                                                let hv = "Hole variable: _" <> occNameString (occName $ hole_occ h)+                                                let ht = "Hole type: " <> showSDoc dflags (ppr $ hole_ty h)+                                                let rc = "Relevant constraints: " <> showSDoc dflags (ppr $ th_relevant_cts hole)+                                                let le = "Local environment (bindings): " <> showSDoc dflags (ppr $ tcl_rdr lcl_env)+                                                let ge = "Global environment (bindings): " <> showSDoc dflags (ppr $ tcg_binds gbl_env)+                                                let cf = "Candidate fits: " <> showSDoc dflags (ppr fits)+                                                let prompt' =+                                                        replacePlaceholders+                                                            promptTemplate+                                                            [ ("{module}", mn)+                                                            , ("{location}", lc)+                                                            , ("{imports}", im)+                                                            , ("{hole_var}", hv)+                                                            , ("{hole_type}", ht)+                                                            , ("{relevant_constraints}", rc)+                                                            , ("{local_env}", le)+                                                            , ("{global_env}", ge)+                                                            , ("{candidate_fits}", cf)+                                                            , ("{numexpr}", show num_expr)+                                                            ]+                                                res <- Ollama.generate genOps{prompt = prompt', modelName = model_name}+                                                case res of+                                                    Right rsp -> do+                                                        let lns = (preProcess . T.lines) $ Ollama.response_ rsp+                                                        when debug $ do+                                                            T.putStrLn $ "--- Ollama Plugin: Prompt ---\n" <> prompt'+                                                            T.putStrLn $ "--- Ollama Plugin: Response ---\n" <> Ollama.response_ rsp+                                                        let fits' = map (RawHoleFit . text . T.unpack) lns+                                                        -- Return the generated fits+                                                        return fits'+                                                    Left err -> do+                                                        when debug $+                                                            putStrLn $+                                                                "Ollama plugin failed to generate a response.\n" <> err+                                                        -- Return the original fits without modification+                                                        return fits+                                            Nothing -> return fits+                                }+                    }+        }++-- | Preprocess the response to remove empty lines, lines with only spaces, and code blocks+preProcess :: [Text] -> [Text]+preProcess [] = []+preProcess (ln : lns) | should_drop = preProcess lns+  where+    should_drop :: Bool+    should_drop =+        T.null ln+            || T.all isSpace ln+            || T.isPrefixOf "```" ln+preProcess (ln : lns) = transform ln : preProcess lns+  where+    transform :: Text -> Text+    transform = id++-- | Command line options for the plugin+data Flags = Flags+    { model_name :: Text+    , num_expr :: Int+    , debug :: Bool+    }++-- | Default flags for the plugin+defaultFlags :: Flags+defaultFlags =+    Flags+        { model_name = "gemma3:27b-it-qat"+        , num_expr = 5+        , debug = False+        }++-- | Parse command line options+parseFlags :: [CommandLineOption] -> Flags+parseFlags = parseFlags' defaultFlags+  where+    parseFlags' :: Flags -> [CommandLineOption] -> Flags+    parseFlags' flags [] = flags+    parseFlags' flags (opt : opts)+        | T.isPrefixOf "model=" (T.pack opt) =+            let model_name = T.drop (T.length "model=") (T.pack opt)+             in parseFlags' flags{model_name = model_name} opts+    parseFlags' flags (opt : opts)+        | T.isPrefixOf "debug=" (T.pack opt) =+            let debug = T.unpack $ T.drop (T.length "debug=") (T.pack opt)+             in parseFlags' flags{debug = read debug} opts+    parseFlags' flags (opt : opts)+        | T.isPrefixOf "n=" (T.pack opt) =+            let num_expr = T.unpack $ T.drop (T.length "n=") (T.pack opt)+             in parseFlags' flags{num_expr = read num_expr} opts+    parseFlags' flags _ = flags++-- | Helper function to replace placeholders in a template string+replacePlaceholders :: Text -> [(Text, String)] -> Text+replacePlaceholders = foldl replacePlaceholder+  where+    replacePlaceholder :: Text -> (Text, String) -> Text+    replacePlaceholder str (placeholder, value) = T.replace placeholder (T.pack value) str