diff --git a/package.yaml b/package.yaml index 9e82980..c82b358 100644 --- a/package.yaml +++ b/package.yaml @@ -46,6 +46,7 @@ dependencies: - time - wai - warp + - yaml library: source-dirs: src diff --git a/roux-server.cabal b/roux-server.cabal index 22a1224..8c6e267 100644 --- a/roux-server.cabal +++ b/roux-server.cabal @@ -18,6 +18,7 @@ library exposed-modules: Data.CookLang Roux + Roux.Config Roux.CooklangPrint Roux.Html Roux.NYTimes @@ -58,6 +59,7 @@ library , time , wai , warp + , yaml default-language: Haskell2010 executable check-recipes @@ -95,6 +97,7 @@ executable check-recipes , time , wai , warp + , yaml default-language: Haskell2010 executable roux-server @@ -133,6 +136,7 @@ executable roux-server , unix , wai , warp + , yaml default-language: Haskell2010 test-suite roux-server-test @@ -179,4 +183,5 @@ test-suite roux-server-test , time , wai , warp + , yaml default-language: Haskell2010 diff --git a/src/Roux/Config.hs b/src/Roux/Config.hs new file mode 100644 index 0000000..bbf9c63 --- /dev/null +++ b/src/Roux/Config.hs @@ -0,0 +1,72 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} + +{- | Configuration for Roux, loaded from a YAML config file. + +Default location: ./roux-config.yaml in the working directory. +Overridable via --config-file CLI flag. +-} +module Roux.Config ( + RouxConfig (..), + AnthropicConfig (..), + loadConfig, +) where + +import Data.Text (Text) +import qualified Data.Yaml as Y +import qualified Fleece.Aeson as Fleece +import qualified Fleece.Aeson.Decoder as FleeceD +import Fleece.Core (constructor, object, optionalNullable, required, text, (#+)) +import qualified Fleece.Core as FC +import System.Directory (doesFileExist) + +-- | Top-level config. +data RouxConfig = RouxConfig + { rcAnthropic :: Maybe AnthropicConfig + } + deriving stock (Eq, Show) + +-- | Anthropic API configuration. +data AnthropicConfig = AnthropicConfig + { acApiKey :: !Text + , acBaseUrl :: !(Maybe Text) -- default: https://api.anthropic.com + , acModel :: !(Maybe Text) -- default: claude-sonnet-4-20250514 + } + deriving stock (Eq, Show) + +-- | Fleece schema for AnthropicConfig. +anthropicConfigSchema :: FC.Schema Fleece.Decoder AnthropicConfig +anthropicConfigSchema = + object $ + constructor AnthropicConfig + #+ required "api_key" acApiKey text + #+ optionalNullable FC.EmitNull "base_url" acBaseUrl text + #+ optionalNullable FC.EmitNull "model" acModel text + +-- | Fleece schema for RouxConfig. +rouxConfigSchema :: FC.Schema Fleece.Decoder RouxConfig +rouxConfigSchema = + object $ + constructor RouxConfig + #+ optionalNullable FC.EmitNull "anthropic" rcAnthropic anthropicConfigSchema + +{- | Load config from a YAML file. Returns config with nothing set if the file +doesn't exist (LLM features will be unavailable but the server still runs). +Exits with an error if the file exists but is malformed. +-} +loadConfig :: FilePath -> IO RouxConfig +loadConfig path = do + exists <- doesFileExist path + if not exists + then pure (RouxConfig Nothing) + else do + result <- Y.decodeFileEither path + case result of + Left parseErr -> + fail $ "Failed to parse " <> path <> ": " <> show parseErr + Right val -> + case FleeceD.fromValue rouxConfigSchema val of + Right cfg -> pure cfg + Left decodeErr -> + fail $ + "Invalid config format in " <> path <> ": " <> show decodeErr