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 +30/−0
- README.md +53/−0
- Setup.hs +2/−0
- openai-hs.cabal +81/−0
- src/OpenAI/Client.hs +84/−0
- src/OpenAI/Client/Internal/Helpers.hs +23/−0
- test/ApiSpec.hs +63/−0
- test/HelperSpec.hs +88/−0
- test/Spec.hs +10/−0
+ 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