Using the OpenAI Large Language Model APIs in Haskell

Here we will use the library openai-hs written by Alexander Thiemann. The GitHub repository for his library is https://github.com/agrafix/openai-hs/tree/main/openai-hs.

We will start by writing client code to call the OpenAI completion API for an arbitrary input text. We will then use this completion client code for a specialized application: finding all the place names in input text and returning them as a list of strings.

In the development of practical AI systems, LLMs like those provided by OpenAI, Anthropic, and Hugging Face have emerged as pivotal tools for numerous applications including natural language processing, generation, and understanding. These models, powered by deep learning architectures, encapsulate a wealth of knowledge and computational capabilities. As a Haskell enthusiast embarking on the journey of intertwining the elegance of Haskell with the power of these modern language models, you might also want to experiment with the OpenAI Python examples that are much more complete than what we look at here.

OpenAI provides an API for developers to access models like GPT-4o. The OpenAI API is designed with simplicity and ease of use in mind, making it a common choice for developers. It provides endpoints for different types of interactions, be it text completion, translation, or semantic search among others. We will use the text completion API in this chapter. The robustness and versatility of the OpenAI API make it a valuable asset for anyone looking to integrate advanced language understanding and generation capabilities into their applications.

While we use the GPT-4o model here, you can substitute the following models:

  • GPT-5 - expensive to run, for complex multi-step tasks
  • GPT-5-mini - inexpensive to run, for simpler tasks (this is the default model I use)
  • o1 - very expensive to run, most capable model that has massive knowledge of the real world and can solve complex multi-step reasoning problems.
  • o1-mini - slightly less expensive to run than o1-preview, less real world knowledge and simpler reasoning capabilities.

Example Client Code

This Haskell program demonstrates how to interact with the OpenAI ChatCompletion API using the Openai-hs library. The code sends a prompt to the OpenAI API and prints the assistant’s response to the console. It’s a practical example of how to set up an OpenAI client, create a request, handle the response, and manage potential errors in a Haskell application.

Firstly, the code imports necessary modules and libraries. It imports OpenAI.Client for interacting with the OpenAI API and Network.HTTP.Client along with Network.HTTP.Client.TLS for handling HTTP requests over TLS. The System.Environment module is used to access environment variables, specifically to retrieve the OpenAI API key. Additionally, Data.Text is imported for efficient text manipulation, and Data.Maybe is used for handling optional values.

The core of the program is the completionRequestToString function. This function takes a String argument prompt and returns an IO String, representing the assistant’s response.

What is an IO String? In Haskell, IO String represents an action that, when executed, produces a String, whereas String is simply a value.

  • IO String: A computation in the IO monad that will produce a String when executed.
  • String: A pure value, just a sequence of characters.

You can’t directly extract a String from IO String; you need to perform the IO action (e.g., using main or inside the do notation) to get the result.

Inside function completionRequestToString, an HTTP manager with TLS support is created using newManager tlsManagerSettings. Then, it retrieves the OpenAI API key from the OPENAI_KEY environment variable using getEnv “OPENAI_KEY” and packs it into a Text type with T.pack.

An OpenAI client is instantiated using makeOpenAIClient, passing the API key, the HTTP manager, and an integer 4, which represents a maximum number of retries. The code then constructs a ChatCompletionRequest, specifying the model to use (in this case, ModelId “gpt-4o”) and the messages to send. The messages consist of a single ChatMessage with the user’s prompt, setting chmContent to Just (T.pack prompt) and chmRole to “user”. All other optional parameters in the request are left as Nothing, implying default values will be used.

The function then sends the chat completion request using completeChat client request and pattern matches on the result to handle both success and failure cases. If the request fails (Left failure), it returns a string representation of the failure. On success (Right success), it extracts the assistant’s reply from the chrChoices field. It unpacks the content from Text to String, handling the case where content might be absent by providing a default message “No content”.

Finally, the function main serves as the entry point of the program. It calls completionRequestToString with the prompt “Write a hello world program in Haskell” and prints the assistant’s response using putStrLn. This demonstrates how to use the function in a real-world scenario, providing a complete example of sending a prompt to the OpenAI API and displaying the result.

 1 {-# LANGUAGE OverloadedStrings #-}
 2 import OpenAI.Client
 3 
 4 import Network.HTTP.Client
 5 import Network.HTTP.Client.TLS
 6 import System.Environment (getEnv)
 7 import qualified Data.Text as T
 8 import Data.Maybe (fromMaybe)
 9 import Data.Text (splitOn)
10 
11 -- Example using openai-hs for chat completions
12 -- Requires `OPENAI_KEY` in your environment (e.g., `export OPENAI_KEY=sk-...`)
13 
14 -- Sends a chat prompt and returns the assistant's text as String
15 completionRequestToString :: String -> IO String
16 completionRequestToString prompt = do
17     -- Create an HTTPS-capable connection manager
18     manager <- newManager tlsManagerSettings
19     -- Read your OpenAI API key from the environment
20     apiKey <- T.pack <$> getEnv "OPENAI_KEY"
21     -- Build a client; the last argument (4) retries on transient network errors
22     let client = makeOpenAIClient apiKey manager 4
23     -- Describe the chat request to send
24     let request = ChatCompletionRequest
25                  { chcrModel = ModelId "gpt-5-mini"  -- model to use
26                  , chcrMessages =
27                     [ ChatMessage
28                         { chmContent = Just (T.pack prompt)  -- user prompt
29                         , chmRole = "user"
30                         , chmFunctionCall = Nothing
31                         , chmName = Nothing
32                         }
33                     ]
34                  , chcrFunctions = Nothing
35                  , chcrTemperature = Nothing
36                  , chcrTopP = Nothing
37                  , chcrN = Nothing
38                  , chcrStream = Nothing
39                  , chcrStop = Nothing
40                  , chcrMaxTokens = Nothing
41                  , chcrPresencePenalty = Nothing
42                  , chcrFrequencyPenalty = Nothing
43                  , chcrLogitBias = Nothing
44                  , chcrUser = Nothing
45                  }
46     -- Perform the API call
47     result <- completeChat client request
48     -- Unpack the result and extract the text content from the first choice
49     case result of
50         Left failure -> return (show failure)
51         Right success ->
52             case chrChoices success of
53                 (ChatChoice {chchMessage = ChatMessage {chmContent = content}} : _) ->
54                     return $ fromMaybe "No content" $ T.unpack <$> content
55                 _ -> return "No choices returned"
56 
57 -- find place names
58 -- Extracts place names from `text` (comma-separated) using the chat model
59 findPlaces :: String -> IO [String]
60 findPlaces text = do
61     -- Construct the extraction prompt
62     let prompt = "Extract only the place names separated by commas from the following text:\n\n" ++ text
63     response <- completionRequestToString prompt 
64     -- Convert Text to String using T.unpack before filtering
65     let places = filter (not . null) $ map T.unpack $ splitOn "," (T.pack response) 
66     -- Strip leading and trailing whitespace from each place name
67     return $ map (T.unpack . T.strip . T.pack) places
68 
69 -- Extracts person names from `text` (comma-separated) using the chat model
70 findPeople :: String -> IO [String]
71 findPeople text = do
72     let prompt = "Extract only the person names separated by commas from the following text:\n\n" ++ text
73     response <- completionRequestToString prompt
74     let people = filter (not . null) $ map T.unpack $ splitOn "," (T.pack response)
75     return $ map (T.unpack . T.strip . T.pack) people
76 
77 -- Demo: generate text, then extract places and people
78 main :: IO ()
79 main = do
80     -- Generic text generation
81     response <- completionRequestToString "Write a hello world program in Haskell"
82     putStrLn response
83 
84     -- Extract place names
85     places <- findPlaces "I visited London, Paris, and New York last year."
86     print places
87 
88     -- Extract person names
89     people <- findPeople "John Smith met with Sarah Johnson and Michael Brown at the conference."
90     print people

Here is sample output generated by the gpt-5-mini OpenAI model:

 1 $ cabal build
 2 Build profile: -w ghc-9.8.1 -O1
 3 $ cabal run  
 4 Sure! Here is a simple "Hello, World!" program in Haskell:
 5 
 6   main :: IO ()
 7   main = putStrLn "Hello, World!"
 8 
 9 Explanation:
10 - `main :: IO ()` declares that `main` is an I/O action that returns no meaningful value (indicated by `()`).
11 - `putStrLn "Hello, World!"` is the I/O action that outputs the string "Hello, World!" followed by a newline.
12 
13 To run this program:
14 1. Save the code in a file, for example, `hello.hs`.
15 2. Open a terminal.
16 3. Navigate to the directory containing `hello.hs`.
17 4. Run the program using the Haskell compiler/interpreter (GHC). You can do this by running:
18 
19    runhaskell hello.hs
20 
21 or by compiling it and then running the executable:
22 
23    ghc -o hello hello.hs
24    ./hello

For completeness, here is a partial listing of the OpenAiApiClient.cabal file:

 1 name:                OpenAiApiClient
 2 version:             0.1.0.0
 3 author:              Mark watson and the author of OpenAI Client library Alexander Thiemann
 4 build-type:          Simple
 5 cabal-version:       >=1.10
 6 
 7 executable GenText
 8   hs-source-dirs:      .
 9   main-is:             GenText.hs
10   default-language:    Haskell2010
11   build-depends:       base >= 4.7 && < 5, mtl >= 2.2.2, text, http-client >= 0.7.13.1, openai-hs, http-client-tls

Adding a Simple Application: Find Place Names in Input Text

The example file GenText.hs contains a small application example that uses the function completionRequestToString prompt that we defined in the last section.

Here we define a new function:

1 findPlaces :: String -> IO [String]
2 findPlaces text = do
3     let prompt = "Extract only the place names separated by commas from the following text:\n\n" ++ text
4     response <- completionRequestToString prompt 
5     -- Convert Text to String using T.unpack before filtering
6     let places = filter (not . null) $ map T.unpack $ splitOn "," (T.pack response) 
7     -- Strip leading and trailing whitespace from each place name
8     return $ map (T.unpack . T.strip . T.pack) places

The function findPlaces extracts a list of place names from a given text using an LLM (Large Language Model).

  • It constructs a prompt instructing the LLM to extract only comma-separated place names.
  • It sends this prompt to the LLM using the completionRequestToString function.
  • It processes the LLM’s response, splitting it into a list of potential place names, filtering out empty entries, and stripping leading/trailing whitespace.
  • It returns the final list of extracted place names.

You should use the function findPlaces as a template for prompting the OpenAI completion models like GPT-4o to perform specific tasks.

Given the example code:

1 main :: IO ()
2 main = do
3     places <- findPlaces "I visited London, Paris, and New York last year."
4     print places 

The output would look like:

1 ["London","Paris","New York"]