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-4o - expensive to run, for complex multi-step tasks
- GPT-4o-mini - inexpensive to run, for simpler tasks (this is the default model I use)
- o1-preview - 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 derived from the openai-client library documentation
12
13 completionRequestToString :: String -> IO String
14 completionRequestToString prompt = do
15 manager <- newManager tlsManagerSettings
16 apiKey <- T.pack <$> getEnv "OPENAI_KEY"
17 let client = makeOpenAIClient apiKey manager 4
18 let request = ChatCompletionRequest
19 { chcrModel = ModelId "gpt-4o"
20 , chcrMessages =
21 [ ChatMessage
22 { chmContent = Just (T.pack prompt)
23 , chmRole = "user"
24 , chmFunctionCall = Nothing
25 , chmName = Nothing
26 }
27 ]
28 , chcrFunctions = Nothing
29 , chcrTemperature = Nothing
30 , chcrTopP = Nothing
31 , chcrN = Nothing
32 , chcrStream = Nothing
33 , chcrStop = Nothing
34 , chcrMaxTokens = Nothing
35 , chcrPresencePenalty = Nothing
36 , chcrFrequencyPenalty = Nothing
37 , chcrLogitBias = Nothing
38 , chcrUser = Nothing
39 }
40 result <- completeChat client request
41 case result of
42 Left failure -> return (show failure)
43 Right success ->
44 case chrChoices success of
45 (ChatChoice {chchMessage = ChatMessage {chmContent = content}} : _) \
46 ->
47 return $ fromMaybe "No content" $ T.unpack <$> content
48 _ -> return "No choices returned"
49
50 main :: IO ()
51 main = do
52 response <- completionRequestToString "Write a hello world program in Haskell"
53 putStrLn response
Here is sample output generated by the gpt-4o 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 v\
11 alue (indicated by `()`).
12 - `putStrLn "Hello, World!"` is the I/O action that outputs the string "Hello, World\
13 !" followed by a newline.
14
15 To run this program:
16 1. Save the code in a file, for example, `hello.hs`.
17 2. Open a terminal.
18 3. Navigate to the directory containing `hello.hs`.
19 4. Run the program using the Haskell compiler/interpreter (GHC). You can do this by \
20 running:
21
22 runhaskell hello.hs
23
24 or by compiling it and then running the executable:
25
26 ghc -o hello hello.hs
27 ./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 T\
4 hiemann
5 build-type: Simple
6 cabal-version: >=1.10
7
8 executable GenText
9 hs-source-dirs: .
10 main-is: GenText.hs
11 default-language: Haskell2010
12 build-depends: base >= 4.7 && < 5, mtl >= 2.2.2, text, http-client >= 0.7.13\
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 followin\
4 g text:\n\n" ++ text
5 response <- completionRequestToString prompt
6 -- Convert Text to String using T.unpack before filtering
7 let places = filter (not . null) $ map T.unpack $ splitOn "," (T.pack response)
8 -- Strip leading and trailing whitespace from each place name
9 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"]