Skip to content

Commit 8c1163d

Browse files
committed
Add OpenAPI specification and swagger-ui
In order for LLMs to interact with the REST API (which is on the goals of the so-called LAG project), we need to provide an openapi3 compatible API. We do this by depending on 'servant-openapi3', and add appropriate instances for the servant REST API. Due to some issues, it seems like it is impossible (or very difficult), to make custom gpts invoke REST endpoints with JSON bodies... So, for now we simply send all parameters via Query Parameters. Until the next servant release, we have to explicitly name all query parameters. See haskell-servant/servant#1604 for the PR that we are interested in.
1 parent 56fc3bb commit 8c1163d

File tree

5 files changed

+648
-241
lines changed

5 files changed

+648
-241
lines changed

Diff for: lib/haskell/explainable/app/Main.hs

+14-237
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,30 @@
11
{-# LANGUAGE DataKinds #-}
2-
{-# LANGUAGE DeriveAnyClass #-}
3-
{-# LANGUAGE DerivingStrategies #-}
4-
{-# LANGUAGE LambdaCase #-}
5-
{-# LANGUAGE OverloadedStrings #-}
6-
{-# LANGUAGE QuasiQuotes #-}
7-
{-# LANGUAGE TypeOperators #-}
82

93
module Main (main) where
104

11-
import Control.Monad.IO.Class (MonadIO, liftIO)
12-
import Control.Monad.Trans.Except
13-
import Data.Aeson (FromJSON, ToJSON, (.=))
14-
import Data.Aeson qualified as Aeson
15-
import Data.Aeson.Types qualified as Aeson
16-
import Data.HashMap.Strict qualified as HashMap
17-
import Data.Map qualified as Map
18-
import Data.Scientific (toRealFloat)
19-
import Data.String.Interpolate (__i)
20-
import Data.Text qualified as Text
21-
import Explainable.MathLang
22-
import GHC.Generics
235
import Network.Wai
246
import Network.Wai.Handler.Warp
257
import Network.Wai.Logger
268
import Options
27-
import Options.Applicative
9+
import Options.Applicative as Opts
10+
import Schema
2811
import Servant
29-
import System.Timeout (timeout)
12+
import Servant.Swagger.UI (SwaggerSchemaUI, swaggerSchemaUIServer)
13+
import Server
3014

3115
-- ----------------------------------------------------------------------------
3216
-- Option Parser
3317
-- ----------------------------------------------------------------------------
3418

3519
opts :: ParserInfo Options
3620
opts =
37-
info
21+
Opts.info
3822
(optionsParser <**> helper)
3923
( fullDesc
4024
<> progDesc "Serve a Web Service for interacting with a MathLang evaluator"
4125
<> header "explainable - A web server for MathLang"
4226
)
4327

44-
-- ----------------------------------------------------------------------------
45-
-- Servant API
46-
-- ----------------------------------------------------------------------------
47-
48-
type Api = FunctionApi
49-
50-
type FunctionApi =
51-
"functions"
52-
:> (Functions :<|> FunctionsCrud)
53-
54-
type Functions = Get '[JSON] [SimpleFunction]
55-
56-
type FunctionsCrud =
57-
Capture "name" String
58-
:> ( ReqBody '[JSON] Attributes :> Post '[JSON] SimpleResponse
59-
:<|> Get '[JSON] Function
60-
)
61-
62-
data FlatValue
63-
= Number Double
64-
| Boolean Bool
65-
deriving (Show, Read, Ord, Eq, Generic)
66-
67-
instance FromJSON FlatValue where
68-
parseJSON (Aeson.Number sci) = pure $ Number $ toRealFloat sci
69-
parseJSON (Aeson.Bool b) = pure $ Boolean b
70-
parseJSON o =
71-
Aeson.parseFail $ "Unexpected value, expected Number or Bool but got: " <> show o
72-
73-
newtype Attributes = Attributes
74-
{ mkAttributes :: Map.Map String FlatValue
75-
}
76-
deriving (Show, Read, Ord, Eq, Generic)
77-
deriving newtype (FromJSON)
78-
79-
data SimpleResponse
80-
= SimpleResponse Double
81-
| Insufficient String
82-
deriving (Show, Read, Ord, Eq, Generic)
83-
deriving anyclass (FromJSON, ToJSON)
84-
8528
-- ----------------------------------------------------------------------------
8629
-- Main Application and wiring
8730
-- ----------------------------------------------------------------------------
@@ -93,180 +36,14 @@ main = do
9336
let settings = setPort port $ setLogger aplogger defaultSettings
9437
runSettings settings app
9538

96-
app :: Application
97-
app = serve (Proxy @Api) handler
98-
99-
-- ----------------------------------------------------------------------------
100-
-- Web Service Handlers
101-
-- ----------------------------------------------------------------------------
102-
103-
handler :: Server Api
104-
handler =
105-
handlerFunctions
106-
:<|> ( \name ->
107-
handlerFunction name
108-
:<|> handlerParameters name
109-
)
110-
111-
handlerFunctions :: Handler [SimpleFunction]
112-
handlerFunctions = do
113-
pure $ fmap (toSimpleFunction . snd) $ Map.elems functions
114-
where
115-
toSimpleFunction s =
116-
SimpleFunction
117-
{ simpleName = name s
118-
, simpleDescription = description s
119-
}
120-
121-
handlerFunction :: String -> Attributes -> Handler SimpleResponse
122-
handlerFunction name query = do
123-
case Map.lookup name functions of
124-
Nothing -> throwError err404
125-
Just (scenario, _) ->
126-
case runExcept $ fromParams query of
127-
Left err ->
128-
pure $ Insufficient err
129-
Right s -> do
130-
response <- timeoutAction $ runScenario s scenario
131-
pure $ SimpleResponse response
39+
type ApiWithSwagger =
40+
SwaggerSchemaUI "swagger-ui" "swagger.json"
41+
:<|> Api
13242

133-
handlerParameters :: String -> Handler Function
134-
handlerParameters name = case Map.lookup name functions of
135-
Nothing -> throwError err404
136-
Just (_, scenario) -> pure scenario
43+
appWithSwagger :: Servant.Server ApiWithSwagger
44+
appWithSwagger =
45+
swaggerSchemaUIServer serverOpenApi
46+
:<|> handler
13747

138-
timeoutAction :: IO b -> Handler b
139-
timeoutAction act =
140-
liftIO (timeout (seconds 5) act) >>= \case
141-
Nothing -> throwError err505
142-
Just r -> pure r
143-
where
144-
seconds n = 1_000_000 * n
145-
146-
-- ----------------------------------------------------------------------------
147-
-- API specification for LLMs
148-
-- ----------------------------------------------------------------------------
149-
150-
data SimpleFunction = SimpleFunction
151-
{ simpleName :: Text.Text
152-
, simpleDescription :: Text.Text
153-
}
154-
deriving (Show, Read, Ord, Eq, Generic)
155-
156-
data Function = Function
157-
{ name :: Text.Text
158-
, description :: Text.Text
159-
, parameters :: Parameters
160-
}
161-
deriving (Show, Read, Ord, Eq, Generic)
162-
163-
newtype Parameters = Parameters Properties
164-
deriving (Show, Read, Ord, Eq, Generic)
165-
166-
instance ToJSON SimpleFunction where
167-
toJSON (SimpleFunction n desc) =
168-
Aeson.object
169-
[ "type" .= Aeson.String "function"
170-
, "function"
171-
.= Aeson.object
172-
[ "name" .= Aeson.String n
173-
, "description" .= Aeson.String desc
174-
]
175-
]
176-
177-
instance ToJSON Function where
178-
toJSON (Function n desc params) =
179-
Aeson.object
180-
[ "type" .= Aeson.String "function"
181-
, "function"
182-
.= Aeson.object
183-
[ "name" .= Aeson.String n
184-
, "description" .= Aeson.String desc
185-
, "parameters" .= params
186-
]
187-
]
188-
189-
instance ToJSON Parameters where
190-
toJSON (Parameters props) =
191-
Aeson.object
192-
[ "type" .= Aeson.String "object"
193-
, "properties" .= props
194-
]
195-
196-
newtype Properties = Properties
197-
{ mkProperties :: Map.Map String Parameter
198-
}
199-
deriving (Show, Read, Ord, Eq, Generic)
200-
deriving newtype (ToJSON)
201-
202-
data Parameter = Parameter
203-
{ parameterType :: String
204-
, parameterEnum :: [String]
205-
, parameterDescription :: String
206-
}
207-
deriving (Show, Read, Ord, Eq, Generic)
208-
209-
instance ToJSON Parameter where
210-
toJSON (Parameter ty enum desc) =
211-
Aeson.object
212-
[ "type" .= ty
213-
, "enum" .= enum
214-
, "description" .= desc
215-
]
216-
217-
-- ----------------------------------------------------------------------------
218-
-- Example Rules
219-
-- ----------------------------------------------------------------------------
220-
221-
runScenario :: (MonadIO m) => MyState -> Expr Double -> m Double
222-
runScenario s scenario = do
223-
(res, _, _, _) <- liftIO $ xplainF () s scenario
224-
pure res
225-
226-
fromParams :: Attributes -> Except String MyState
227-
fromParams attrs = do
228-
let (valueMap, predMap) = Map.mapEither go (mkAttributes attrs)
229-
pure $
230-
emptyState
231-
{ symtabF = HashMap.fromList $ Map.toList valueMap
232-
, symtabP = HashMap.fromList $ Map.toList predMap
233-
}
234-
where
235-
go (Number n) = Left $ Val Nothing n
236-
go (Boolean b) = Right $ PredVal Nothing b
237-
238-
functions :: Map.Map String (Expr Double, Function)
239-
functions =
240-
Map.fromList
241-
[ ("compute_qualifies", (personQualifies, personQualifiesFunction))
242-
]
243-
244-
personQualifies :: Expr Double
245-
personQualifies =
246-
"qualifies"
247-
@|= MathPred
248-
( (getvar "walks") |&& ((getvar "drinks") ||| (getvar "eats"))
249-
)
250-
251-
personQualifiesFunction :: Function
252-
personQualifiesFunction =
253-
Function
254-
"compute_qualifies"
255-
[__i|Determines if a person qualifies for the purposes of the rule.
256-
The input object describes the person's properties in the primary parameters: walks, eats, drinks.
257-
Secondary parameters can be given which are sufficient to determine some of the primary parameters.
258-
A person drinks whether or not they consume an alcoholic or a non-alcoholic beverage, in part or in whole;
259-
those specific details don't really matter.
260-
The output of the function can be either a request for required information;
261-
a restatement of the user input requesting confirmation prior to function calling;
262-
or a Boolean answer with optional explanation summary.
263-
|]
264-
$ Parameters
265-
$ Properties
266-
$ Map.fromList
267-
[ ("walks", Parameter "string" ["true", "false", "unknown"] "Did the person walk?")
268-
, ("eats", Parameter "string" ["true", "false", "unknown"] "Did the person eat?")
269-
, ("drinks", Parameter "string" ["true", "false", "unknown"] "Did the person drink?")
270-
, ("beverage type", Parameter "string" ["alcoholic", "non-alcoholic", "unknown"] "Did the person drink an alcoholic beverage?")
271-
, ("in whole", Parameter "string" ["true", "false", "unknown"] "Did the person drink all of the beverage?")
272-
]
48+
app :: Application
49+
app = serve (Proxy @ApiWithSwagger) appWithSwagger

0 commit comments

Comments
 (0)