Added JSON config via CLI for Wasp AI.

This commit is contained in:
Martin Sosic 2023-06-29 21:25:23 +02:00
parent 005c17cef6
commit a5323a2e05
7 changed files with 96 additions and 37 deletions

View File

@ -38,14 +38,17 @@ export const startGeneratingNewApp: StartGeneratingNewApp<
unconsumedStdout: "",
};
// { auth: 'UsernameAndPassword', primaryColor: string }
const projectConfig = {}
const stdoutMutex = new Mutex();
let waspCliProcess = null;
if (process.env.NODE_ENV === "production") {
waspCliProcess = spawn("wasp", ["new-ai", args.appName, args.appDesc]);
waspCliProcess = spawn("wasp", ["new-ai", args.appName, args.appDesc, JSON.stringify(projectConfig)]);
} else {
// NOTE: In dev when we use `wasp-cli`, we want to make sure that if this app is run via `wasp` that its datadir env var does not propagate,
// so we reset it here. This is problem only if you run app with `wasp` and let it call `wasp-cli` here.
waspCliProcess = spawn("wasp-cli", ["new-ai", args.appName, args.appDesc], {
waspCliProcess = spawn("wasp-cli", ["new-ai", args.appName, args.appDesc, JSON.stringify(projectConfig)], {
env: { ...process.env, waspc_datadir: undefined },
});
}

View File

@ -47,11 +47,11 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do
("new" : newArgs) -> Command.Call.New newArgs
-- new-ai / new-ai:stdout is meant to be called and consumed programatically (e.g. by our Wasp AI
-- web app), while new-ai:disk is useful for us for testing.
[newAiCmd, projectName, appDescription]
[newAiCmd, projectName, appDescription, projectConfigJson]
| newAiCmd `elem` ["new-ai", "new-ai:stdout"] ->
Command.Call.NewAiToStdout projectName appDescription
Command.Call.NewAiToStdout projectName appDescription projectConfigJson
| newAiCmd == "new-ai:disk" ->
Command.Call.NewAiToDisk projectName appDescription
Command.Call.NewAiToDisk projectName appDescription projectConfigJson
["start"] -> Command.Call.Start
["start", "db"] -> Command.Call.StartDb
["clean"] -> Command.Call.Clean
@ -86,10 +86,10 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do
case commandCall of
Command.Call.New newArgs -> runCommand $ createNewProject newArgs
Command.Call.NewAiToStdout projectName appDescription ->
runCommand $ Command.CreateNewProject.AI.createNewProjectNonInteractiveToStdout projectName appDescription
Command.Call.NewAiToDisk projectName appDescription ->
runCommand $ Command.CreateNewProject.AI.createNewProjectNonInteractiveOnDisk projectName appDescription
Command.Call.NewAiToStdout projectName appDescription projectConfigJson ->
runCommand $ Command.CreateNewProject.AI.createNewProjectNonInteractiveToStdout projectName appDescription projectConfigJson
Command.Call.NewAiToDisk projectName appDescription projectConfigJson ->
runCommand $ Command.CreateNewProject.AI.createNewProjectNonInteractiveOnDisk projectName appDescription projectConfigJson
Command.Call.Start -> runCommand start
Command.Call.StartDb -> runCommand Command.Start.Db.start
Command.Call.Clean -> runCommand clean

View File

@ -2,8 +2,8 @@ module Wasp.Cli.Command.Call where
data Call
= New Arguments
| NewAiToStdout String String -- projectName, appDescription
| NewAiToDisk String String -- projectName, appDescription
| NewAiToStdout String String String -- projectName, appDescription, projectConfigJson
| NewAiToDisk String String String -- projectName, appDescription, projectConfigJson
| Start
| StartDb
| Clean

View File

@ -7,6 +7,7 @@ where
import Control.Arrow ()
import Control.Monad.Except (MonadError (throwError), MonadIO (liftIO))
import Data.Function ((&))
import qualified Data.Text as T
import qualified Data.Text.IO as T.IO
import StrongPath (Abs, Dir, Path', fromAbsDir)
@ -17,34 +18,44 @@ import System.FilePath (takeDirectory)
import System.IO (hFlush, stdout)
import qualified Wasp.AI.CodeAgent as CA
import qualified Wasp.AI.GenerateNewProject as GNP
import Wasp.AI.GenerateNewProject.Common (AuthProvider (..), NewProjectDetails (..))
import Wasp.AI.GenerateNewProject.Common (NewProjectConfig, NewProjectDetails (..), emptyNewProjectConfig)
import Wasp.AI.OpenAI (OpenAIApiKey)
import Wasp.Cli.Command (Command, CommandError (CommandError))
import Wasp.Cli.Command.CreateNewProject.ProjectDescription (NewProjectAppName (..), obtainAvailableProjectDirPath, parseWaspProjectNameIntoAppName)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates (readWaspProjectSkeletonFiles)
import Wasp.Cli.Common (WaspProjectDir)
import qualified Wasp.Cli.Interactive as Interactive
import qualified Wasp.Util.Aeson as Utils.Aeson
createNewProjectInteractiveOnDisk :: Path' Abs (Dir WaspProjectDir) -> NewProjectAppName -> Command ()
createNewProjectInteractiveOnDisk waspProjectDir appName = do
openAIApiKey <- getOpenAIApiKey
appDescription <- liftIO $ Interactive.askForRequiredInput "Describe your app in a couple of sentences"
liftIO $ createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription
liftIO $ createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription emptyNewProjectConfig
createNewProjectNonInteractiveOnDisk :: String -> String -> Command ()
createNewProjectNonInteractiveOnDisk projectName appDescription = do
createNewProjectNonInteractiveOnDisk :: String -> String -> String -> Command ()
createNewProjectNonInteractiveOnDisk projectName appDescription projectConfigJson = do
appName <- case parseWaspProjectNameIntoAppName projectName of
Right appName -> pure appName
Left err -> throwError $ CommandError "Invalid project name" err
projectConfig <-
Utils.Aeson.decodeFromString projectConfigJson
& either (throwError . CommandError "Invalid project config" . ("Failed to parse JSON: " <>)) pure
waspProjectDir <- obtainAvailableProjectDirPath projectName
openAIApiKey <- getOpenAIApiKey
liftIO $ createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription
liftIO $ createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription projectConfig
createNewProjectOnDisk :: OpenAIApiKey -> Path' Abs (Dir WaspProjectDir) -> NewProjectAppName -> String -> IO ()
createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription = do
createNewProjectOnDisk ::
OpenAIApiKey ->
Path' Abs (Dir WaspProjectDir) ->
NewProjectAppName ->
String ->
NewProjectConfig ->
IO ()
createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription projectConfig = do
createDirectory $ fromAbsDir waspProjectDir
setCurrentDirectory $ fromAbsDir waspProjectDir
generateNewProject codeAgentConfig appName appDescription
generateNewProject codeAgentConfig appName appDescription projectConfig
where
codeAgentConfig =
CA.CodeAgentConfig
@ -65,14 +76,18 @@ createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription = do
-- | Instead of writing files to disk, it will write files (and logs) to the stdout,
-- with delimiters that make it easy to programmaticaly parse the output.
createNewProjectNonInteractiveToStdout :: String -> String -> Command ()
createNewProjectNonInteractiveToStdout projectName appDescription = do
createNewProjectNonInteractiveToStdout :: String -> String -> String -> Command ()
createNewProjectNonInteractiveToStdout projectName appDescription projectConfigJsonStr = do
openAIApiKey <- getOpenAIApiKey
appName <- case parseWaspProjectNameIntoAppName projectName of
Right appName -> pure appName
Left err -> throwError $ CommandError "Invalid project name" err
projectConfig <-
Utils.Aeson.decodeFromString projectConfigJsonStr
& either (throwError . CommandError "Invalid project config" . ("Failed to parse JSON: " <>)) pure
let codeAgentConfig =
CA.CodeAgentConfig
{ CA._openAIApiKey = openAIApiKey,
@ -80,7 +95,7 @@ createNewProjectNonInteractiveToStdout projectName appDescription = do
CA._writeLog = writeLogToStdoutWithDelimiters
}
liftIO $ generateNewProject codeAgentConfig appName appDescription
liftIO $ generateNewProject codeAgentConfig appName appDescription projectConfig
where
writeFileToStdoutWithDelimiters path content =
writeToStdoutWithDelimiters "WRITE FILE" [T.pack path, content]
@ -100,11 +115,11 @@ createNewProjectNonInteractiveToStdout projectName appDescription = do
"===/ WASP AI: " <> title <> " ===="
]
generateNewProject :: CA.CodeAgentConfig -> NewProjectAppName -> String -> IO ()
generateNewProject codeAgentConfig (NewProjectAppName appName) appDescription = do
generateNewProject :: CA.CodeAgentConfig -> NewProjectAppName -> String -> NewProjectConfig -> IO ()
generateNewProject codeAgentConfig (NewProjectAppName appName) appDescription projectConfig = do
waspProjectSkeletonFiles <- readWaspProjectSkeletonFiles
CA.runCodeAgent codeAgentConfig $ do
GNP.generateNewProject (newProjectDetails appName appDescription) waspProjectSkeletonFiles
GNP.generateNewProject (newProjectDetails projectConfig appName appDescription) waspProjectSkeletonFiles
getOpenAIApiKey :: Command OpenAIApiKey
getOpenAIApiKey =
@ -123,10 +138,10 @@ getOpenAIApiKey =
"to .bash_profile or .profile, restart your shell, and you should be good to go."
]
newProjectDetails :: String -> String -> NewProjectDetails
newProjectDetails webAppName webAppDescription =
newProjectDetails :: NewProjectConfig -> String -> String -> NewProjectDetails
newProjectDetails projectConfig webAppName webAppDescription =
NewProjectDetails
{ _projectAppName = webAppName,
_projectDescription = webAppDescription,
_projectAuth = UsernameAndPassword
_projectConfig = projectConfig
}

View File

@ -1,14 +1,18 @@
module Wasp.AI.GenerateNewProject.Common
( NewProjectDetails (..),
File,
NewProjectConfig (..),
AuthProvider (..),
File,
getProjectAuth,
getProjectPrimaryColor,
emptyNewProjectConfig,
queryChatGPTForJSON,
defaultChatGPTParams,
writeToWaspFileEnd,
)
where
import Data.Aeson (FromJSON)
import Data.Aeson (FromJSON, withObject, withText, (.:?))
import qualified Data.Aeson as Aeson
import Data.Maybe (fromMaybe)
import Data.Text (Text)
@ -21,14 +25,46 @@ import Wasp.Util (naiveTrimJSON, textToLazyBS)
data NewProjectDetails = NewProjectDetails
{ _projectAppName :: !String,
_projectDescription :: !String,
_projectAuth :: !AuthProvider
_projectConfig :: NewProjectConfig
}
-- TODO: Make these relative to WaspProjectDir, via StrongPath?
type File = (FilePath, Text)
data NewProjectConfig = NewProjectConfig
{ projectAuth :: !(Maybe AuthProvider),
-- CSS acceptable string for color.
projectPrimaryColor :: !(Maybe String)
}
deriving (Show)
instance Aeson.FromJSON NewProjectConfig where
parseJSON = withObject "NewProjectConfig" $ \obj -> do
auth <- obj .:? "auth"
primaryColor <- obj .:? "primaryColor"
return (NewProjectConfig {projectAuth = auth, projectPrimaryColor = primaryColor})
emptyNewProjectConfig :: NewProjectConfig
emptyNewProjectConfig =
NewProjectConfig
{ projectAuth = Nothing,
projectPrimaryColor = Nothing
}
getProjectAuth :: NewProjectDetails -> AuthProvider
getProjectAuth = fromMaybe UsernameAndPassword . projectAuth . _projectConfig
getProjectPrimaryColor :: NewProjectDetails -> String
getProjectPrimaryColor = fromMaybe "#fc0" . projectPrimaryColor . _projectConfig
-- TODO: Support more methods.
data AuthProvider = UsernameAndPassword
deriving (Show)
instance Aeson.FromJSON AuthProvider where
parseJSON = withText "AuthProvider" $ \case
"UsernameAndPassword" -> return UsernameAndPassword
_ -> fail "invalid auth provider"
-- TODO: Make these relative to WaspProjectDir, via StrongPath?
type File = (FilePath, Text)
queryChatGPTForJSON :: FromJSON a => ChatGPTParams -> [ChatMessage] -> CodeAgent a
queryChatGPTForJSON chatGPTParams = doQueryForJSON 0

View File

@ -11,7 +11,7 @@ import StrongPath (File', Path, Rel)
import qualified StrongPath as SP
import StrongPath.Types (System)
import Wasp.AI.CodeAgent (CodeAgent, writeNewFile)
import Wasp.AI.GenerateNewProject.Common (AuthProvider (..), File, NewProjectDetails (..))
import Wasp.AI.GenerateNewProject.Common (AuthProvider (..), File, NewProjectDetails (..), getProjectAuth)
import Wasp.AI.GenerateNewProject.Plan (PlanRule)
import Wasp.Project (WaspProjectDir)
import qualified Wasp.SemanticVersion as SV
@ -28,7 +28,7 @@ generateAndWriteProjectSkeletonAndPresetFiles newProjectDetails waspProjectSkele
let (waspFile@(waspFilePath, _), planRules) = generateBaseWaspFile newProjectDetails
writeNewFile waspFile
case _projectAuth newProjectDetails of
case getProjectAuth newProjectDetails of
UsernameAndPassword -> do
writeNewFile generateLoginJsPage
writeNewFile generateSignupJsPage
@ -50,7 +50,7 @@ generateBaseWaspFile newProjectDetails = ((path, content), planRules)
appName = T.pack $ _projectAppName newProjectDetails
appTitle = appName
waspVersionRange = T.pack . show $ SV.backwardsCompatibleWith Wasp.Version.waspVersion
(appAuth, authPlanRules) = case _projectAuth newProjectDetails of
(appAuth, authPlanRules) = case getProjectAuth newProjectDetails of
UsernameAndPassword ->
( [trimming|
auth: {

View File

@ -1,13 +1,18 @@
module Wasp.Util.Aeson
( encodeToText,
decodeFromString,
)
where
import Data.Aeson (ToJSON)
import Data.Aeson (FromJSON, ToJSON, eitherDecode)
import Data.Aeson.Text (encodeToTextBuilder)
import qualified Data.ByteString.Lazy.UTF8 as BS
import Data.Text (Text)
import Data.Text.Lazy (toStrict)
import Data.Text.Lazy.Builder (toLazyText)
encodeToText :: ToJSON a => a -> Text
encodeToText = toStrict . toLazyText . encodeToTextBuilder
decodeFromString :: FromJSON a => String -> Either String a
decodeFromString = eitherDecode . BS.fromString