packages feed

openai-hs 0.2.1.0 → 0.2.2.0

raw patch · 8 files changed

+325/−226 lines, 8 filessetup-changedPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- OpenAI.Client: FileHunk :: Text -> Maybe Text -> FileHunk
- OpenAI.Client: [fhContent] :: FileHunk -> Text
- OpenAI.Client: [fhMetadata] :: FileHunk -> Maybe Text
+ OpenAI.Client: ClassificationHunk :: Text -> Text -> ClassificationHunk
+ OpenAI.Client: Embedding :: Vector Double -> Int -> Embedding
+ OpenAI.Client: EmbeddingCreate :: Text -> EmbeddingCreate
+ OpenAI.Client: FhClassifications :: ClassificationHunk -> FileHunk
+ OpenAI.Client: FhFineTune :: FineTuneHunk -> FileHunk
+ OpenAI.Client: FhSearch :: SearchHunk -> FileHunk
+ OpenAI.Client: FineTune :: FineTuneId -> Text -> Int -> Vector FineTuneEvent -> Maybe Text -> Text -> FineTune
+ OpenAI.Client: FineTuneCreate :: FileId -> Maybe FileId -> Maybe Text -> Maybe Int -> Maybe Text -> Maybe Double -> Maybe Double -> Maybe Bool -> Maybe Int -> Maybe Text -> FineTuneCreate
+ OpenAI.Client: FineTuneEvent :: Int -> Text -> Text -> FineTuneEvent
+ OpenAI.Client: FineTuneHunk :: Text -> Text -> FineTuneHunk
+ OpenAI.Client: FineTuneId :: Text -> FineTuneId
+ OpenAI.Client: SearchHunk :: Text -> Maybe Text -> SearchHunk
+ OpenAI.Client: [chLabel] :: ClassificationHunk -> Text
+ OpenAI.Client: [chText] :: ClassificationHunk -> Text
+ OpenAI.Client: [eEmbedding] :: Embedding -> Vector Double
+ OpenAI.Client: [eIndex] :: Embedding -> Int
+ OpenAI.Client: [ecInput] :: EmbeddingCreate -> Text
+ OpenAI.Client: [ftCreatedAt] :: FineTune -> Int
+ OpenAI.Client: [ftEvents] :: FineTune -> Vector FineTuneEvent
+ OpenAI.Client: [ftId] :: FineTune -> FineTuneId
+ OpenAI.Client: [ftModel] :: FineTune -> Text
+ OpenAI.Client: [ftStatus] :: FineTune -> Text
+ OpenAI.Client: [ftTunedModel] :: FineTune -> Maybe Text
+ OpenAI.Client: [ftcBatchSize] :: FineTuneCreate -> Maybe Int
+ OpenAI.Client: [ftcClassificationNClasses] :: FineTuneCreate -> Maybe Int
+ OpenAI.Client: [ftcClassificationPositiveClass] :: FineTuneCreate -> Maybe Text
+ OpenAI.Client: [ftcComputeClassificationMetrics] :: FineTuneCreate -> Maybe Bool
+ OpenAI.Client: [ftcLearningRateMultiplier] :: FineTuneCreate -> Maybe Double
+ OpenAI.Client: [ftcModel] :: FineTuneCreate -> Maybe Text
+ OpenAI.Client: [ftcNEpochs] :: FineTuneCreate -> Maybe Text
+ OpenAI.Client: [ftcPromptLossWeight] :: FineTuneCreate -> Maybe Double
+ OpenAI.Client: [ftcTrainingFile] :: FineTuneCreate -> FileId
+ OpenAI.Client: [ftcValidationFile] :: FineTuneCreate -> Maybe FileId
+ OpenAI.Client: [fteCreatedAt] :: FineTuneEvent -> Int
+ OpenAI.Client: [fteLevel] :: FineTuneEvent -> Text
+ OpenAI.Client: [fteMessage] :: FineTuneEvent -> Text
+ OpenAI.Client: [fthCompletion] :: FineTuneHunk -> Text
+ OpenAI.Client: [fthPrompt] :: FineTuneHunk -> Text
+ OpenAI.Client: [shMetadata] :: SearchHunk -> Maybe Text
+ OpenAI.Client: [shText] :: SearchHunk -> Text
+ OpenAI.Client: [unFineTuneId] :: FineTuneId -> Text
+ OpenAI.Client: cancelFineTune :: OpenAIClient -> FineTuneId -> IO (Either ClientError FineTune)
+ OpenAI.Client: createEmbedding :: OpenAIClient -> EngineId -> EmbeddingCreate -> IO (Either ClientError (OpenAIList Embedding))
+ OpenAI.Client: createFineTune :: OpenAIClient -> FineTuneCreate -> IO (Either ClientError FineTune)
+ OpenAI.Client: data ClassificationHunk
+ OpenAI.Client: data Embedding
+ OpenAI.Client: data EmbeddingCreate
+ OpenAI.Client: data FineTune
+ OpenAI.Client: data FineTuneCreate
+ OpenAI.Client: data FineTuneEvent
+ OpenAI.Client: data FineTuneHunk
+ OpenAI.Client: data SearchHunk
+ OpenAI.Client: defaultFineTuneCreate :: FileId -> FineTuneCreate
+ OpenAI.Client: getFineTune :: OpenAIClient -> FineTuneId -> IO (Either ClientError FineTune)
+ OpenAI.Client: listFineTuneEvents :: OpenAIClient -> FineTuneId -> IO (Either ClientError (OpenAIList FineTuneEvent))
+ OpenAI.Client: listFineTunes :: OpenAIClient -> IO (Either ClientError (OpenAIList FineTune))
+ OpenAI.Client: newtype FineTuneId

Files

LICENSE view
@@ -1,4 +1,4 @@-Copyright Author name here (c) 2020+Copyright Author name here (c) 2021-2022  All rights reserved. 
Setup.hs view
@@ -1,2 +1,3 @@ import Distribution.Simple+ main = defaultMain
openai-hs.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 1dde99a185b23e2392f6b88ffc6c086ea7240054bcf59d9848a2bd6f36ecfabc+-- hash: 8760d75e39a4a6db941c2503a8e8519a3c82b52bad91dcf39594cdd9258c7f4b  name:           openai-hs-version:        0.2.1.0+version:        0.2.2.0 synopsis:       Unofficial OpenAI client description:    Unofficial OpenAI client category:       Web@@ -15,7 +15,7 @@ bug-reports:    https://github.com/agrafix/openai-hs/issues author:         Alexander Thiemann <mail@thiemann.at> maintainer:     Alexander Thiemann <mail@thiemann.at>-copyright:      2021 Alexander Thiemann <mail@thiemann.at>+copyright:      2021-2022 Alexander Thiemann <mail@thiemann.at> license:        BSD3 license-file:   LICENSE build-type:     Simple
src/OpenAI/Client.hs view
@@ -1,61 +1,101 @@-{-# OPTIONS_GHC -cpp -pgmPcpphs -optP--cpp #-} {-# LANGUAGE CPP #-}+{-# OPTIONS_GHC -cpp -pgmPcpphs -optP--cpp #-}+ module OpenAI.Client   ( -- * Basics-    ApiKey, OpenAIClient, makeOpenAIClient, ClientError(..)+    ApiKey,+    OpenAIClient,+    makeOpenAIClient,+    ClientError (..),+     -- * Helper types-  , TimeStamp(..), OpenAIList(..)+    TimeStamp (..),+    OpenAIList (..),+     -- * Engine-  , EngineId(..), Engine(..)-  , listEngines-  , getEngine+    EngineId (..),+    Engine (..),+    listEngines,+    getEngine,+     -- * Text completion-  , TextCompletionId(..), TextCompletionChoice(..), TextCompletion(..), TextCompletionCreate(..)-  , defaultTextCompletionCreate-  , completeText+    TextCompletionId (..),+    TextCompletionChoice (..),+    TextCompletion (..),+    TextCompletionCreate (..),+    defaultTextCompletionCreate,+    completeText,++    -- * Embeddings+    EmbeddingCreate (..),+    Embedding (..),+    createEmbedding,++    -- * Fine tunes+    FineTuneId (..),+    FineTuneCreate (..),+    defaultFineTuneCreate,+    FineTune (..),+    FineTuneEvent (..),+    createFineTune,+    listFineTunes,+    getFineTune,+    cancelFineTune,+    listFineTuneEvents,+     -- * Searching-  , SearchResult(..), SearchResultCreate(..)-  , searchDocuments+    SearchResult (..),+    SearchResultCreate (..),+    searchDocuments,+     -- * File API-  , FileCreate(..), File(..), FileId(..), FileHunk(..)-  , FileDeleteConfirmation(..)-  , createFile, deleteFile+    FileCreate (..),+    File (..),+    FileId (..),+    FileHunk (..),+    SearchHunk (..),+    ClassificationHunk (..),+    FineTuneHunk (..),+    FileDeleteConfirmation (..),+    createFile,+    deleteFile,+     -- * Answer API-  , getAnswer, AnswerReq(..), AnswerResp(..)+    getAnswer,+    AnswerReq (..),+    AnswerResp (..),   ) where +import qualified Data.ByteString.Lazy as BSL+import Data.Proxy+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Network.HTTP.Client (Manager) import OpenAI.Api import OpenAI.Client.Internal.Helpers import OpenAI.Resources--import Data.Proxy-import Network.HTTP.Client (Manager) import Servant.API import Servant.Client-import qualified Data.ByteString.Lazy as BSL-import qualified Data.Text as T-import qualified Data.Text.Encoding as T import qualified Servant.Multipart.Client as MP  -- | Your OpenAI API key. Can be obtained from the OpenAI dashboard. Format: @sk-<redacted>@ type ApiKey = T.Text  -- | Holds a 'Manager' and your API key.-data OpenAIClient-  = OpenAIClient-  { scBasicAuthData :: BasicAuthData-  , scManager :: Manager-  , scMaxRetries :: Int+data OpenAIClient = OpenAIClient+  { scBasicAuthData :: BasicAuthData,+    scManager :: Manager,+    scMaxRetries :: Int   }  -- | Construct a 'OpenAIClient'. Note that the passed 'Manager' must support https (e.g. via @http-client-tls@) makeOpenAIClient ::-  ApiKey-  -> Manager-  -> Int-  -- ^ Number of automatic retries the library should attempt.-  -> OpenAIClient+  ApiKey ->+  Manager ->+  -- | Number of automatic retries the library should attempt.+  Int ->+  OpenAIClient makeOpenAIClient k = OpenAIClient (BasicAuthData "" (T.encodeUtf8 k))  api :: Proxy OpenAIApi@@ -79,26 +119,42 @@     N :: OpenAIClient -> ARG -> ARG2 -> IO (Either ClientError R);\     N sc a b = runRequest (scMaxRetries sc) 0 $ runClientM (N##' (scBasicAuthData sc) a b) (mkClientEnv (scManager sc) openaiBaseUrl) -EP2(completeText, EngineId, TextCompletionCreate, TextCompletion)-EP2(searchDocuments, EngineId, SearchResultCreate, (OpenAIList SearchResult))+EP2 (completeText, EngineId, TextCompletionCreate, TextCompletion)+EP2 (searchDocuments, EngineId, SearchResultCreate, (OpenAIList SearchResult))+EP2 (createEmbedding, EngineId, EmbeddingCreate, (OpenAIList Embedding)) -EP0(listEngines, (OpenAIList Engine))-EP(getEngine, EngineId, Engine)+EP (createFineTune, FineTuneCreate, FineTune)+EP0 (listFineTunes, (OpenAIList FineTune))+EP (getFineTune, FineTuneId, FineTune)+EP (cancelFineTune, FineTuneId, FineTune)+EP (listFineTuneEvents, FineTuneId, (OpenAIList FineTuneEvent)) +EP0 (listEngines, (OpenAIList Engine))+EP (getEngine, EngineId, Engine)+ createFile :: OpenAIClient -> FileCreate -> IO (Either ClientError File) createFile sc rfc =-  do bnd <- MP.genBoundary-     createFileInternal sc (bnd, rfc)+  do+    bnd <- MP.genBoundary+    createFileInternal sc (bnd, rfc) -EP(createFileInternal, (BSL.ByteString, FileCreate), File)-EP(deleteFile, FileId, FileDeleteConfirmation)+EP (createFileInternal, (BSL.ByteString, FileCreate), File)+EP (deleteFile, FileId, FileDeleteConfirmation) -EP(getAnswer, AnswerReq, AnswerResp)+EP (getAnswer, AnswerReq, AnswerResp) -(listEngines'-  :<|> getEngine'-  :<|> completeText'-  :<|> searchDocuments')+( listEngines'+    :<|> getEngine'+    :<|> completeText'+    :<|> searchDocuments'+    :<|> createEmbedding'+  )   :<|> (createFileInternal' :<|> deleteFile')   :<|> getAnswer'-  = client api+  :<|> ( createFineTune'+           :<|> listFineTunes'+           :<|> getFineTune'+           :<|> cancelFineTune'+           :<|> listFineTuneEvents'+         ) =+    client api
src/OpenAI/Client/Internal/Helpers.hs view
@@ -1,5 +1,6 @@--- | Private helper functions. Note that all contents of this module are excluded from the versioning scheme. {-# LANGUAGE BangPatterns #-}++-- | Private helper functions. Note that all contents of this module are excluded from the versioning scheme. module OpenAI.Client.Internal.Helpers where  import Network.HTTP.Types.Status@@ -7,17 +8,18 @@  runRequest :: Int -> Int -> IO (Either ClientError a) -> IO (Either ClientError a) runRequest maxRetries !retryCount makeRequest =-  do res <- makeRequest-     case res of-       Right ok -> pure (Right ok)-       Left err@(ConnectionError _) -> maybeRetry err-       Left err@(FailureResponse _ resp)-         | responseStatusCode resp == conflict409 -> maybeRetry err-         | statusCode (responseStatusCode resp) >= 500 -> maybeRetry err-         | otherwise -> pure (Left err)-       Left err -> pure (Left err)+  do+    res <- makeRequest+    case res of+      Right ok -> pure (Right ok)+      Left err@(ConnectionError _) -> maybeRetry err+      Left err@(FailureResponse _ resp)+        | responseStatusCode resp == conflict409 -> maybeRetry err+        | statusCode (responseStatusCode resp) >= 500 -> maybeRetry err+        | otherwise -> pure (Left err)+      Left err -> pure (Left err)   where     maybeRetry err =       if retryCount + 1 >= maxRetries-      then pure (Left err)-      else runRequest maxRetries (retryCount + 1) makeRequest+        then pure (Left err)+        else runRequest maxRetries (retryCount + 1) makeRequest
test/ApiSpec.hs view
@@ -1,26 +1,26 @@ module ApiSpec (apiSpec) where +import qualified Data.Text as T+import qualified Data.Vector as V import Network.HTTP.Client import Network.HTTP.Client.TLS+import OpenAI.Client import System.Environment (getEnv) import Test.Hspec-import qualified Data.Text as T-import qualified Data.Vector as V -import OpenAI.Client- makeClient :: IO OpenAIClient makeClient =-  do manager <- newManager tlsManagerSettings-     apiKey <- T.pack <$> getEnv "OPENAI_KEY"-     pure (makeOpenAIClient apiKey manager 2)+  do+    manager <- newManager tlsManagerSettings+    apiKey <- T.pack <$> getEnv "OPENAI_KEY"+    pure (makeOpenAIClient apiKey manager 2)  forceSuccess :: (MonadFail m, Show a) => m (Either a b) -> m b forceSuccess req =   req >>= \res ->-  case res of-    Left err -> fail (show err)-    Right ok -> pure ok+    case res of+      Left err -> fail (show err)+      Right ok -> pure ok  apiSpec :: Spec apiSpec =@@ -29,97 +29,129 @@ apiTests :: SpecWith () apiTests =   beforeAll makeClient $-  do describe "file api" $-       do it "allows creating one" $ \cli ->-            do let file =-                     FileCreate-                     { fcPurpose = "search"-                     , fcDocuments = [FileHunk "Test 1" Nothing, FileHunk "text 2" (Just "foo")]-                     }-               res <- forceSuccess $ createFile cli file-               _ <- forceSuccess $ deleteFile cli (fId res)-               pure ()-     describe "answer api" $-       do it "works" $ \cli ->-            do let file =-                     FileCreate-                     { fcPurpose = "search"-                     , fcDocuments =-                         [ FileHunk "Cities in California: San Francisco, Los Angeles" (Just "cali")-                         , FileHunk "Tasty fruit: Apple, Orange" (Just "fruit")-                         , FileHunk "Cities in Germany: Freiburg, Berlin" (Just "germany")-                         ]-                     }-               res <- forceSuccess $ createFile cli file-               let searchReq =-                     AnswerReq-                     { arFile = Just (fId res)-                     , arDocuments = Nothing-                     , arQuestion = "Where is San Francisco?"-                     , arSearchModel = EngineId "babbage"-                     , arModel = EngineId "davinci"-                     , arExamplesContext = "Good programming languages: Haskell, PureScript"-                     , arExamples = [["Is PHP a good programming language?", "No, sorry."]]-                     , arReturnMetadata = True-                     }-               answerRes <- forceSuccess $ getAnswer cli searchReq-               T.unpack (head (arsAnswers answerRes)) `shouldContain` ("California" :: String)-               _ <- forceSuccess $ deleteFile cli (fId res)-               pure ()-     describe "engines" $-       do it "lists engines" $ \cli ->-            do res <- forceSuccess $ listEngines cli-               V.null (olData res) `shouldBe` False+    do+      describe "file api" $+        do+          it "allows creating one" $ \cli ->+            do+              let file =+                    FileCreate+                      { fcPurpose = "search",+                        fcDocuments = [FhSearch $ SearchHunk "Test 1" Nothing, FhSearch $ SearchHunk "text 2" (Just "foo")]+                      }+              _ <- forceSuccess $ createFile cli file+              pure ()+      describe "answer api" $+        do+          it "works" $ \cli ->+            do+              let file =+                    FileCreate+                      { fcPurpose = "search",+                        fcDocuments =+                          [ FhSearch $ SearchHunk "Cities in California: San Francisco, Los Angeles" (Just "cali"),+                            FhSearch $ SearchHunk "Tasty fruit: Apple, Orange" (Just "fruit"),+                            FhSearch $ SearchHunk "Cities in Germany: Freiburg, Berlin" (Just "germany")+                          ]+                      }+              res <- forceSuccess $ createFile cli file+              let searchReq =+                    AnswerReq+                      { arFile = Just (fId res),+                        arDocuments = Nothing,+                        arQuestion = "Where is San Francisco?",+                        arSearchModel = EngineId "babbage",+                        arModel = EngineId "davinci",+                        arExamplesContext = "Good programming languages: Haskell, PureScript",+                        arExamples = [["Is PHP a good programming language?", "No, sorry."]],+                        arReturnMetadata = True+                      }+              answerRes <- forceSuccess $ getAnswer cli searchReq+              T.unpack (head (arsAnswers answerRes)) `shouldContain` ("California" :: String)+              pure ()+      describe "embeddings" $ do+        it "computes embeddings" $ \cli -> do+          res <- forceSuccess $ createEmbedding cli (EngineId "ada-similarity") (EmbeddingCreate "This is nice")+          V.null (olData res) `shouldBe` False+          let embedding = V.head (olData res)+          V.length (eEmbedding embedding) `shouldBe` 1024+      describe "fine tuning" $ do+        it "allows creating fine-tuning" $ \cli -> do+          let file =+                FileCreate+                  { fcPurpose = "fine-tune",+                    fcDocuments =+                      [ FhFineTune $ FineTuneHunk "So sad. Label:" "sad",+                        FhFineTune $ FineTuneHunk "So happy. Label:" "happy"+                      ]+                  }+          createRes <- forceSuccess $ createFile cli file+          let ftc = defaultFineTuneCreate (fId createRes)+          res <- forceSuccess $ createFineTune cli ftc+          ftStatus res `shouldBe` "pending"+      describe "engines" $+        do+          it "lists engines" $ \cli ->+            do+              res <- forceSuccess $ listEngines cli+              V.null (olData res) `shouldBe` False           it "retrieve engine" $ \cli ->-            do engineList <- forceSuccess $ listEngines cli-               let firstEngine = V.head (olData engineList)-               engine <- forceSuccess $ getEngine cli (eId firstEngine)-               engine `shouldBe` firstEngine-     describe "text completion" $-       do it "works (smoke test)" $ \cli ->-            do firstEngine <- V.head . olData <$> forceSuccess (listEngines cli)-               completionResults <--                 forceSuccess $-                 completeText cli (eId firstEngine) $-                 (defaultTextCompletionCreate "Why is the house ")-                 { tccrMaxTokens = Just 2 }-               V.length (tcChoices completionResults) `shouldBe` 1-               T.length (tccText (V.head (tcChoices completionResults))) `shouldNotBe` 0-     describe "document search" $-       do it "works (smoke test)" $ \cli ->-            do firstEngine <- V.head . olData <$> forceSuccess (listEngines cli)-               searchResults <--                 forceSuccess $-                 searchDocuments cli (eId firstEngine) $-                 SearchResultCreate-                 { sccrDocuments = Just $ V.fromList ["pool", "gym", "night club"]-                 , sccrFile = Nothing-                 , sccrQuery = "swimmer"-                 , sccrReturnMetadata = False-                 }-               V.length (olData searchResults) `shouldBe` 3-     describe "file based document search" $-       do it "works" $ \cli ->-            do let file =-                     FileCreate-                     { fcPurpose = "search"-                     , fcDocuments =-                         [ FileHunk "pool" (Just "pool")-                         , FileHunk "gym" (Just "gym")-                         , FileHunk "night club" (Just "nc")-                         ]-                     }-               createRes <- forceSuccess $ createFile cli file-               let searchReq =-                     SearchResultCreate-                     { sccrFile = Just (fId createRes)-                     , sccrDocuments = Nothing-                     , sccrQuery = "pool"-                     , sccrReturnMetadata = True-                     }-               searchRes <- forceSuccess $ searchDocuments cli (EngineId "ada") searchReq-               let res = V.head (olData searchRes)-               srDocument res `shouldBe` 0 -- pool-               srMetadata res `shouldBe` Just "pool"-               _ <- forceSuccess $ deleteFile cli (fId createRes)-               pure ()+            do+              engineList <- forceSuccess $ listEngines cli+              let firstEngine = V.head (olData engineList)+              engine <- forceSuccess $ getEngine cli (eId firstEngine)+              engine `shouldBe` firstEngine+      describe "text completion" $+        do+          it "works (smoke test)" $ \cli ->+            do+              firstEngine <- V.head . olData <$> forceSuccess (listEngines cli)+              completionResults <-+                forceSuccess $+                  completeText cli (eId firstEngine) $+                    (defaultTextCompletionCreate "Why is the house ")+                      { tccrMaxTokens = Just 2+                      }+              V.length (tcChoices completionResults) `shouldBe` 1+              T.length (tccText (V.head (tcChoices completionResults))) `shouldNotBe` 0+      describe "document search" $+        do+          it "works (smoke test)" $ \cli ->+            do+              firstEngine <- V.head . olData <$> forceSuccess (listEngines cli)+              searchResults <-+                forceSuccess $+                  searchDocuments cli (eId firstEngine) $+                    SearchResultCreate+                      { sccrDocuments = Just $ V.fromList ["pool", "gym", "night club"],+                        sccrFile = Nothing,+                        sccrQuery = "swimmer",+                        sccrReturnMetadata = False+                      }+              V.length (olData searchResults) `shouldBe` 3+      describe "file based document search" $+        do+          it "works" $ \cli ->+            do+              let file =+                    FileCreate+                      { fcPurpose = "search",+                        fcDocuments =+                          [ FhSearch $ SearchHunk "pool" (Just "pool"),+                            FhSearch $ SearchHunk "gym" (Just "gym"),+                            FhSearch $ SearchHunk "night club" (Just "nc")+                          ]+                      }+              createRes <- forceSuccess $ createFile cli file+              let searchReq =+                    SearchResultCreate+                      { sccrFile = Just (fId createRes),+                        sccrDocuments = Nothing,+                        sccrQuery = "pool",+                        sccrReturnMetadata = True+                      }+              searchRes <- forceSuccess $ searchDocuments cli (EngineId "ada") searchReq+              let res = V.head (olData searchRes)+              srDocument res `shouldBe` 0 -- pool+              srMetadata res `shouldBe` Just "pool"+              pure ()
test/HelperSpec.hs view
@@ -1,88 +1,96 @@ module HelperSpec (helperSpec) where  import Control.Exception.Base+import qualified Data.ByteString as BS import Data.IORef import Data.Maybe+import qualified Data.Sequence as Seq import Network.HTTP.Types.Header import Network.HTTP.Types.Status import Network.HTTP.Types.Version+import OpenAI.Client.Internal.Helpers import Servant.Client import Servant.Client.Core.Request import System.Exit import Test.Hspec-import qualified Data.ByteString as BS-import qualified Data.Sequence as Seq -import OpenAI.Client.Internal.Helpers- helperSpec :: Spec helperSpec =   do describe "retries" retryTests  makeFakeAction ::-  (Int -> Either ClientError a)-  -> IO (IO Int, IO (Either ClientError a))+  (Int -> Either ClientError a) ->+  IO (IO Int, IO (Either ClientError a)) makeFakeAction makeResult =-  do calls <- newIORef 0-     let action =-           do call <- atomicModifyIORef calls $ \i -> (i + 1, i)-              pure (makeResult call)-     pure (readIORef calls, action)+  do+    calls <- newIORef 0+    let action =+          do+            call <- atomicModifyIORef calls $ \i -> (i + 1, i)+            pure (makeResult call)+    pure (readIORef calls, action)  dummyReq :: RequestF () (BaseUrl, BS.ByteString) dummyReq =   defaultRequest-  { requestBody = Nothing-  , requestPath = (fromJust $ parseBaseUrl "api.example.com", "")-  }+    { requestBody = Nothing,+      requestPath = (fromJust $ parseBaseUrl "api.example.com", "")+    }  retryAction ::-  Int-  -> Status-  -> Seq.Seq Header-  -> IO (ClientError, IO Int, IO (Either ClientError Bool))+  Int ->+  Status ->+  Seq.Seq Header ->+  IO (ClientError, IO Int, IO (Either ClientError Bool)) retryAction n status headers =-  do let errResp =-           Response-           { responseStatusCode = status-           , responseHeaders = headers-           , responseHttpVersion = http11-           , responseBody = mempty-           }-         err = FailureResponse dummyReq errResp-     (getCalls, action) <--       makeFakeAction $ \call ->-       if call < n-       then Left $ err-       else Right True-     pure (err, getCalls, action)+  do+    let errResp =+          Response+            { responseStatusCode = status,+              responseHeaders = headers,+              responseHttpVersion = http11,+              responseBody = mempty+            }+        err = FailureResponse dummyReq errResp+    (getCalls, action) <-+      makeFakeAction $ \call ->+        if call < n+          then Left $ err+          else Right True+    pure (err, getCalls, action)  retryTests :: SpecWith () retryTests =-  do it "does not retry on success" $-       do (getCalls, action) <- makeFakeAction (const $ Right True)-          runRequest 10 0 action `shouldReturn` Right True-          getCalls `shouldReturn` 1-     it "retries on connection errors" $-       do (getCalls, action) <--            makeFakeAction $ \call ->+  do+    it "does not retry on success" $+      do+        (getCalls, action) <- makeFakeAction (const $ Right True)+        runRequest 10 0 action `shouldReturn` Right True+        getCalls `shouldReturn` 1+    it "retries on connection errors" $+      do+        (getCalls, action) <-+          makeFakeAction $ \call ->             if call == 0-            then Left (ConnectionError $ toException ExitSuccess)-            else Right True-          runRequest 10 0 action `shouldReturn` Right True-          getCalls `shouldReturn` 2-     it "retries on 409 status code" $-       do (_, getCalls, action) <--            retryAction 1 status409 mempty-          runRequest 10 0 action `shouldReturn` Right True-          getCalls `shouldReturn` 2-     it "retries on 500 status code" $-       do (_, getCalls, action) <--            retryAction 1 status500 mempty-          runRequest 10 0 action `shouldReturn` Right True-          getCalls `shouldReturn` 2-     it "does not retry on status 500 if limit exceeded" $-       do (err, getCalls, action) <--            retryAction 11 status500 mempty-          runRequest 10 0 action `shouldReturn` Left err-          getCalls `shouldReturn` 10+              then Left (ConnectionError $ toException ExitSuccess)+              else Right True+        runRequest 10 0 action `shouldReturn` Right True+        getCalls `shouldReturn` 2+    it "retries on 409 status code" $+      do+        (_, getCalls, action) <-+          retryAction 1 status409 mempty+        runRequest 10 0 action `shouldReturn` Right True+        getCalls `shouldReturn` 2+    it "retries on 500 status code" $+      do+        (_, getCalls, action) <-+          retryAction 1 status500 mempty+        runRequest 10 0 action `shouldReturn` Right True+        getCalls `shouldReturn` 2+    it "does not retry on status 500 if limit exceeded" $+      do+        (err, getCalls, action) <-+          retryAction 11 status500 mempty+        runRequest 10 0 action `shouldReturn` Left err+        getCalls `shouldReturn` 10
test/Spec.hs view
@@ -1,10 +1,10 @@-import Test.Hspec- import ApiSpec import HelperSpec+import Test.Hspec  main :: IO () main =   hspec $-  do apiSpec-     helperSpec+    do+      apiSpec+      helperSpec