packages feed

openai-hs (empty) → 0.1.1.0

raw patch · 9 files changed

+434/−0 lines, 9 filesdep +aesondep +basedep +bytestringsetup-changed

Dependencies added: aeson, base, bytestring, casing, containers, cpphs, hspec, http-client, http-client-tls, http-types, openai-hs, openai-servant, servant, servant-client, servant-client-core, text, vector

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Author name here (c) 2020++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Author name here nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,53 @@+# openai-hs++Unofficial OpenAI SDK/client for Haskell. It's generated via `servant-client` from `openai-servant` with a small amount of hand-written code. Contributions are welcome!++## Install++``` sh+# stack+stack install openai-hs++# cabal+cabal install openai-hs+```++## Example++``` haskell+{-# LANGUAGE OverloadedStrings #-}+import OpenAI.Client++import Network.HTTP.Client+import Network.HTTP.Client.TLS+import System.Environment (getEnv)+import qualified Data.Vector as V++main :: IO ()+main =+  do manager <- newManager tlsManagerSettings+     apiKey <- T.pack <$> getEnv "OPENAI_KEY"+     -- create a openai client that automatically retries up to 4 times on network+     -- errors+     let client = makeOpenAIClient apiKey manager 4+     result <-+         searchDocuments cli (eId firstEngine) $+         SearchResultCreate+         { sccrDocuments = V.fromList ["pool", "gym", "night club"]+         , sccrQuery = "swimmer"+         }+     print result+```++## Features++Supported actions:++* List engines+* Retrieve engine+* Create text completion+* Run semantic search++## Running the tests++You can run all tests with `stack test`. You'll need an OpenAI API Key assigned to the `OPENAI_KEY` environment variable.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ openai-hs.cabal view
@@ -0,0 +1,81 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.33.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: bc145a17f925189f4d8d879579d0e37dc7d796a46ca6bb0303538c1f3f2c1f16++name:           openai-hs+version:        0.1.1.0+synopsis:       Unofficial OpenAI client+description:    Unofficial OpenAI client+category:       Web+homepage:       https://github.com/agrafix/openai-hs#readme+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>+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md++source-repository head+  type: git+  location: https://github.com/agrafix/openai-hs++library+  exposed-modules:+      OpenAI.Client+      OpenAI.Client.Internal.Helpers+  other-modules:+      Paths_openai_hs+  hs-source-dirs:+      src+  default-extensions: OverloadedStrings DataKinds TypeOperators TypeFamilies GADTs FlexibleInstances FlexibleContexts MultiParamTypeClasses StrictData ScopedTypeVariables DeriveGeneric DeriveFunctor+  ghc-options: -Wall -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates+  build-depends:+      aeson+    , base >=4.7 && <5+    , casing+    , cpphs+    , http-client+    , http-types+    , openai-servant+    , servant+    , servant-client+    , text+  default-language: Haskell2010++test-suite openai-hs-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      ApiSpec+      HelperSpec+      Paths_openai_hs+  hs-source-dirs:+      test+  default-extensions: OverloadedStrings DataKinds TypeOperators TypeFamilies GADTs FlexibleInstances FlexibleContexts MultiParamTypeClasses StrictData ScopedTypeVariables DeriveGeneric DeriveFunctor+  ghc-options: -Wall -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      aeson+    , base >=4.7 && <5+    , bytestring+    , casing+    , containers+    , cpphs+    , hspec+    , http-client+    , http-client-tls+    , http-types+    , openai-hs+    , openai-servant+    , servant+    , servant-client+    , servant-client-core+    , text+    , vector+  default-language: Haskell2010
+ src/OpenAI/Client.hs view
@@ -0,0 +1,84 @@+{-# OPTIONS_GHC -cpp -pgmPcpphs -optP--cpp #-}+{-# LANGUAGE CPP #-}+module OpenAI.Client+  ( -- * Basics+    ApiKey, OpenAIClient, makeOpenAIClient, ClientError(..)+    -- * Helper types+  , TimeStamp(..), OpenAIList(..)+    -- * Engine+  , EngineId(..), Engine(..)+  , listEngines+  , getEngine+    -- * Text completion+  , TextCompletionId(..), TextCompletionChoice(..), TextCompletion(..), TextCompletionCreate(..)+  , defaultTextCompletionCreate+  , completeText+    -- * Searching+  , SearchResult(..), SearchResultCreate(..)+  , searchDocuments+  )+where++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.Text as T+import qualified Data.Text.Encoding as T++-- | 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+  }++-- | 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+makeOpenAIClient k = OpenAIClient (BasicAuthData "" (T.encodeUtf8 k))++api :: Proxy OpenAIApi+api = Proxy++openaiBaseUrl :: BaseUrl+openaiBaseUrl = BaseUrl Https "api.openai.com" 443 ""++#define EP0(N, R) \+    N##' :: BasicAuthData -> ClientM R;\+    N :: OpenAIClient -> IO (Either ClientError R);\+    N sc = runRequest (scMaxRetries sc) 0 $ runClientM (N##' (scBasicAuthData sc)) (mkClientEnv (scManager sc) openaiBaseUrl)++#define EP(N, ARG, R) \+    N##' :: BasicAuthData -> ARG -> ClientM R;\+    N :: OpenAIClient -> ARG -> IO (Either ClientError R);\+    N sc a = runRequest (scMaxRetries sc) 0 $ runClientM (N##' (scBasicAuthData sc) a) (mkClientEnv (scManager sc) openaiBaseUrl)++#define EP2(N, ARG, ARG2, R) \+    N##' :: BasicAuthData -> ARG -> ARG2 -> ClientM R;\+    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))++EP0(listEngines, (OpenAIList Engine))+EP(getEngine, EngineId, Engine)++listEngines'+  :<|> getEngine'+  :<|> completeText'+  :<|> searchDocuments'+  = client api
+ src/OpenAI/Client/Internal/Helpers.hs view
@@ -0,0 +1,23 @@+-- | Private helper functions. Note that all contents of this module are excluded from the versioning scheme.+{-# LANGUAGE BangPatterns #-}+module OpenAI.Client.Internal.Helpers where++import Network.HTTP.Types.Status+import Servant.Client++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)+  where+    maybeRetry err =+      if retryCount + 1 >= maxRetries+      then pure (Left err)+      else runRequest maxRetries (retryCount + 1) makeRequest
+ test/ApiSpec.hs view
@@ -0,0 +1,63 @@+module ApiSpec (apiSpec) where++import Network.HTTP.Client+import Network.HTTP.Client.TLS+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)++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++apiSpec :: Spec+apiSpec =+  describe "core api" apiTests++apiTests :: SpecWith ()+apiTests =+  beforeAll makeClient $+  do describe "engines" $+       do it "lists engines" $ \cli ->+            do res <- forceSuccess $ listEngines cli+               V.null (olData res) `shouldBe` False+          -- TODO: This doesn't work for some reason, even the cURL example from the docs fail.+          --it "retrieve engine" $ \cli ->+          --  do engineList <- forceSuccess $ listEngines cli+          --     print engineList+          --     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 = V.fromList ["pool", "gym", "night club"]+                 , sccrQuery = "swimmer"+                 }+               V.length (olData searchResults) `shouldBe` 3
+ test/HelperSpec.hs view
@@ -0,0 +1,88 @@+module HelperSpec (helperSpec) where++import Control.Exception.Base+import Data.IORef+import Data.Maybe+import Network.HTTP.Types.Header+import Network.HTTP.Types.Status+import Network.HTTP.Types.Version+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))+makeFakeAction makeResult =+  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", "")+  }++retryAction ::+  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)++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 ->+            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
+ test/Spec.hs view
@@ -0,0 +1,10 @@+import Test.Hspec++import ApiSpec+import HelperSpec++main :: IO ()+main =+  hspec $+  do apiSpec+     helperSpec