mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-27 19:14:52 +03:00
Actions are now generated.
This commit is contained in:
parent
cbb3962508
commit
9ffc115fcd
@ -56,6 +56,7 @@ getFile path = gets $ H.lookup path . _files
|
|||||||
getAllFiles :: CodeAgent [(FilePath, Text)]
|
getAllFiles :: CodeAgent [(FilePath, Text)]
|
||||||
getAllFiles = gets $ H.toList . _files
|
getAllFiles = gets $ H.toList . _files
|
||||||
|
|
||||||
|
-- TODO: Make it so that if ChatGPT replies with being too busy, we try again.
|
||||||
queryChatGPT :: ChatGPTParams -> [ChatMessage] -> CodeAgent Text
|
queryChatGPT :: ChatGPTParams -> [ChatMessage] -> CodeAgent Text
|
||||||
queryChatGPT params messages = do
|
queryChatGPT params messages = do
|
||||||
key <- asks _openAIApiKey
|
key <- asks _openAIApiKey
|
||||||
|
@ -10,7 +10,9 @@ import Data.Text (Text)
|
|||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import NeatInterpolation (trimming)
|
import NeatInterpolation (trimming)
|
||||||
import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, writeToLog)
|
import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, writeToLog)
|
||||||
|
import Wasp.Cli.Command.AI.GenerateNewProject.Action (Action, generateAndWriteAction)
|
||||||
import Wasp.Cli.Command.AI.GenerateNewProject.Common (NewProjectDetails (..))
|
import Wasp.Cli.Command.AI.GenerateNewProject.Common (NewProjectDetails (..))
|
||||||
|
import Wasp.Cli.Command.AI.GenerateNewProject.Entity (writeEntitiesToWaspFile)
|
||||||
import Wasp.Cli.Command.AI.GenerateNewProject.Plan (generatePlan)
|
import Wasp.Cli.Command.AI.GenerateNewProject.Plan (generatePlan)
|
||||||
import qualified Wasp.Cli.Command.AI.GenerateNewProject.Plan as Plan
|
import qualified Wasp.Cli.Command.AI.GenerateNewProject.Plan as Plan
|
||||||
import Wasp.Cli.Command.AI.GenerateNewProject.Skeleton (generateAndWriteProjectSkeleton)
|
import Wasp.Cli.Command.AI.GenerateNewProject.Skeleton (generateAndWriteProjectSkeleton)
|
||||||
@ -25,18 +27,18 @@ import Wasp.Cli.Command.AI.GenerateNewProject.Skeleton (generateAndWriteProjectS
|
|||||||
-- and also contain description of what happened (or maybe that is separate message).
|
-- and also contain description of what happened (or maybe that is separate message).
|
||||||
generateNewProject :: NewProjectDetails -> CodeAgent ()
|
generateNewProject :: NewProjectDetails -> CodeAgent ()
|
||||||
generateNewProject newProjectDetails = do
|
generateNewProject newProjectDetails = do
|
||||||
waspFilePath <- generateAndWriteProjectSkeleton newProjectDetails
|
(waspFilePath, planRules) <- generateAndWriteProjectSkeleton newProjectDetails
|
||||||
writeToLog "Generated project skeleton."
|
writeToLog "Generated project skeleton."
|
||||||
|
|
||||||
writeToLog "Generating plan..."
|
writeToLog "Generating plan..."
|
||||||
plan <- generatePlan newProjectDetails
|
plan <- generatePlan newProjectDetails planRules
|
||||||
writeToLog $ "Plan generated!\n" <> summarizePlan plan
|
writeToLog $ "Plan generated!\n" <> summarizePlan plan
|
||||||
|
|
||||||
writeEntitiesToWaspFile waspFilePath (Plan.entities plan)
|
writeEntitiesToWaspFile waspFilePath (Plan.entities plan)
|
||||||
writeToLog "Added entities to wasp file."
|
writeToLog "Added entities to wasp file."
|
||||||
|
|
||||||
writeToLog "Generating actions..."
|
writeToLog "Generating actions..."
|
||||||
actions <- forM (Plan.actions plan) $ generateAndWriteAction waspFilePath plan
|
actions <- forM (Plan.actions plan) $ generateAndWriteAction newProjectDetails waspFilePath plan
|
||||||
|
|
||||||
writeToLog "Generating queries..."
|
writeToLog "Generating queries..."
|
||||||
queries <- forM (Plan.queries plan) $ generateAndWriteQuery waspFilePath plan
|
queries <- forM (Plan.queries plan) $ generateAndWriteQuery waspFilePath plan
|
||||||
@ -70,16 +72,9 @@ generateNewProject newProjectDetails = do
|
|||||||
showT :: Show a => a -> Text
|
showT :: Show a => a -> Text
|
||||||
showT = T.pack . show
|
showT = T.pack . show
|
||||||
|
|
||||||
generateAndWriteAction waspFilePath plan actionPlan = do
|
|
||||||
action <- generateAction newProjectDetails (Plan.entities plan) actionPlan
|
|
||||||
writeActionToFile action
|
|
||||||
writeActionToWaspFile waspFilePath action
|
|
||||||
writeToLog $ "Generated action: " <> T.pack (Plan.actionName actionPlan)
|
|
||||||
return action
|
|
||||||
|
|
||||||
generateAndWriteQuery waspFilePath plan queryPlan = do
|
generateAndWriteQuery waspFilePath plan queryPlan = do
|
||||||
query <- generateQuery newProjectDetails (Plan.entities plan) queryPlan
|
query <- generateQuery newProjectDetails (Plan.entities plan) queryPlan
|
||||||
writeQueryToFile query
|
writeQueryToFile query -- TODO: Do we need to convert @server/ from path into src/server/?
|
||||||
writeQueryToWaspFile waspFilePath query
|
writeQueryToWaspFile waspFilePath query
|
||||||
writeToLog $ "Generated query: " <> T.pack (Plan.queryName queryPlan)
|
writeToLog $ "Generated query: " <> T.pack (Plan.queryName queryPlan)
|
||||||
return query
|
return query
|
||||||
@ -91,26 +86,6 @@ generateNewProject newProjectDetails = do
|
|||||||
writeToLog $ "Generated page: " <> T.pack (Plan.pageName pagePlan)
|
writeToLog $ "Generated page: " <> T.pack (Plan.pageName pagePlan)
|
||||||
return page
|
return page
|
||||||
|
|
||||||
writeEntitiesToWaspFile :: FilePath -> [Plan.Entity] -> CodeAgent ()
|
|
||||||
writeEntitiesToWaspFile waspFilePath entities = do
|
|
||||||
-- TODO: assemble code for each entity and write it to wasp file.
|
|
||||||
undefined
|
|
||||||
|
|
||||||
generateAction :: NewProjectDetails -> [Plan.Entity] -> Plan.Action -> CodeAgent Action
|
|
||||||
generateAction = undefined
|
|
||||||
|
|
||||||
writeActionToFile :: Action -> CodeAgent ()
|
|
||||||
writeActionToFile = undefined
|
|
||||||
|
|
||||||
writeActionToWaspFile :: FilePath -> Action -> CodeAgent ()
|
|
||||||
writeActionToWaspFile waspFilePath action = undefined
|
|
||||||
|
|
||||||
data Action = Action
|
|
||||||
{ _actionWaspDecl :: String,
|
|
||||||
_actionJsImpl :: String,
|
|
||||||
_actionPlan :: Plan.Action
|
|
||||||
}
|
|
||||||
|
|
||||||
generateQuery :: NewProjectDetails -> [Plan.Entity] -> Plan.Query -> CodeAgent Query
|
generateQuery :: NewProjectDetails -> [Plan.Entity] -> Plan.Query -> CodeAgent Query
|
||||||
generateQuery = undefined
|
generateQuery = undefined
|
||||||
|
|
||||||
|
149
waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Action.hs
Normal file
149
waspc/cli/src/Wasp/Cli/Command/AI/GenerateNewProject/Action.hs
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
|
||||||
|
module Wasp.Cli.Command.AI.GenerateNewProject.Action
|
||||||
|
( generateAndWriteAction,
|
||||||
|
Action,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Data.Aeson (FromJSON)
|
||||||
|
import Data.List (isPrefixOf)
|
||||||
|
import Data.Maybe (fromMaybe)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import GHC.Generics (Generic)
|
||||||
|
import NeatInterpolation (trimming)
|
||||||
|
import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, writeToFile, writeToLog)
|
||||||
|
import Wasp.Cli.Command.AI.GenerateNewProject.Common
|
||||||
|
( NewProjectDetails (..),
|
||||||
|
defaultChatGPTParams,
|
||||||
|
queryChatGPTForJSON,
|
||||||
|
)
|
||||||
|
import Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts (appDescriptionStartMarkerLine)
|
||||||
|
import qualified Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts as Prompts
|
||||||
|
import Wasp.Cli.Command.AI.GenerateNewProject.Entity (entityPlanToWaspDecl)
|
||||||
|
import Wasp.Cli.Command.AI.GenerateNewProject.Plan (Plan)
|
||||||
|
import qualified Wasp.Cli.Command.AI.GenerateNewProject.Plan as Plan
|
||||||
|
import Wasp.OpenAI.ChatGPT (ChatMessage (..), ChatRole (..))
|
||||||
|
|
||||||
|
generateAndWriteAction :: NewProjectDetails -> FilePath -> Plan -> Plan.Action -> CodeAgent Action
|
||||||
|
generateAndWriteAction newProjectDetails waspFilePath plan actPlan = do
|
||||||
|
action <- generateAction newProjectDetails (Plan.entities plan) actPlan
|
||||||
|
writeActionToJsFile action
|
||||||
|
writeActionToWaspFile waspFilePath action
|
||||||
|
writeToLog $ "Generated action: " <> T.pack (Plan.actionName actPlan)
|
||||||
|
return action
|
||||||
|
|
||||||
|
generateAction :: NewProjectDetails -> [Plan.Entity] -> Plan.Action -> CodeAgent Action
|
||||||
|
generateAction newProjectDetails entityPlans plan = do
|
||||||
|
impl <- queryChatGPTForJSON defaultChatGPTParams chatMessages
|
||||||
|
return Action {actionImpl = impl, actionPlan = plan}
|
||||||
|
where
|
||||||
|
chatMessages =
|
||||||
|
[ ChatMessage {role = System, content = Prompts.systemPrompt},
|
||||||
|
ChatMessage {role = User, content = planPrompt}
|
||||||
|
]
|
||||||
|
appName = T.pack $ _projectAppName newProjectDetails
|
||||||
|
appDesc = T.pack $ _projectDescription newProjectDetails
|
||||||
|
basicWaspLangInfoPrompt = Prompts.basicWaspLangInfo
|
||||||
|
actionName = T.pack $ Plan.actionName plan
|
||||||
|
actionFnPath = T.pack $ Plan.actionFnPath plan
|
||||||
|
actionDesc = T.pack $ Plan.actionDesc plan
|
||||||
|
entityDecls = T.intercalate "\n\n" $ entityPlanToWaspDecl <$> entityPlans
|
||||||
|
planPrompt =
|
||||||
|
[trimming|
|
||||||
|
${basicWaspLangInfoPrompt}
|
||||||
|
|
||||||
|
${actionDocPrompt}
|
||||||
|
|
||||||
|
We are implementing a Wasp app (check bottom for description).
|
||||||
|
|
||||||
|
This app has following entities:
|
||||||
|
${entityDecls}
|
||||||
|
|
||||||
|
Let's now implement the following Wasp action:
|
||||||
|
- name: ${actionName}
|
||||||
|
- fn: ${actionFnPath}
|
||||||
|
- description: ${actionDesc}
|
||||||
|
|
||||||
|
Please, respond ONLY with a valid JSON, of following format:
|
||||||
|
{ "actionWaspDecl": "action ${actionName} {\n ... }",
|
||||||
|
"actionJsImpl": "export const {$actionName} ... ",
|
||||||
|
"actionJsImports": "import foo from \"bar.js\"\n ...""
|
||||||
|
}
|
||||||
|
"waspDeclaration" and "jsImplementation" are required, "jsImports" you can skip if none are needed.
|
||||||
|
There should be no other text in the response.
|
||||||
|
|
||||||
|
${appDescriptionStartMarkerLine}
|
||||||
|
|
||||||
|
App name: ${appName}
|
||||||
|
${appDesc}
|
||||||
|
|]
|
||||||
|
actionDocPrompt =
|
||||||
|
[trimming|
|
||||||
|
Action is implemented via Wasp declaration and corresponding NodeJS implementation.
|
||||||
|
|
||||||
|
Example of Wasp declaration:
|
||||||
|
|
||||||
|
```wasp
|
||||||
|
action updateTaskIsDone {
|
||||||
|
fn: import { updateTaskIsDone } from "@server/taskActions.js",
|
||||||
|
entities: [Task] // Entities that action uses.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example of NodeJS implementation:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import HttpError from '@wasp/core/HttpError.js'
|
||||||
|
|
||||||
|
export const updateTaskIsDone = (args, context) => {
|
||||||
|
if (!context.user) { throw new HttpError(403) }
|
||||||
|
|
||||||
|
return context.entities.Task.update({ // prisma object
|
||||||
|
where: { args.id },
|
||||||
|
data: { args.isDone }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Action can then be easily called from the client, via Wasp's RPC mechanism.
|
||||||
|
|]
|
||||||
|
|
||||||
|
writeActionToJsFile :: Action -> CodeAgent ()
|
||||||
|
writeActionToJsFile action =
|
||||||
|
-- TODO: An issue we have here is that if other action already did the same import,
|
||||||
|
-- we don't know and we import it again.
|
||||||
|
-- One thing we can do it supply chatGPT with a list of imports that are already there.
|
||||||
|
-- Second thing we can do is to look for same lines at the start of the file, but that sounds fragile.
|
||||||
|
-- Maybe best to read and pass previous imports (we would have to do that above somewhere).
|
||||||
|
-- Or even the whole file? Hmmmmm.
|
||||||
|
writeToFile path $
|
||||||
|
((jsImports <> "\n") <>) . (<> "\n\n" <> jsImpl) . (fromMaybe "")
|
||||||
|
where
|
||||||
|
path = resolvePath $ Plan.actionFnPath $ actionPlan action
|
||||||
|
jsImpl = T.pack $ actionJsImpl $ actionImpl action
|
||||||
|
jsImports = T.pack $ actionJsImports $ actionImpl action
|
||||||
|
resolvePath p | "@server/" `isPrefixOf` p = "src/server/" <> drop (length ("@server/" :: String)) p
|
||||||
|
resolvePath _ = error "path incorrectly formatted, should start with @server."
|
||||||
|
|
||||||
|
writeActionToWaspFile :: FilePath -> Action -> CodeAgent ()
|
||||||
|
writeActionToWaspFile waspFilePath action =
|
||||||
|
writeToFile waspFilePath $
|
||||||
|
(<> "\n\n" <> waspDeclCode) . fromMaybe (error "wasp file shouldn't be empty")
|
||||||
|
where
|
||||||
|
waspDeclCode = T.pack $ actionWaspDecl $ actionImpl action
|
||||||
|
|
||||||
|
data Action = Action
|
||||||
|
{ actionImpl :: ActionImpl,
|
||||||
|
actionPlan :: Plan.Action
|
||||||
|
}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
data ActionImpl = ActionImpl
|
||||||
|
{ actionWaspDecl :: String,
|
||||||
|
actionJsImpl :: String,
|
||||||
|
actionJsImports :: String
|
||||||
|
}
|
||||||
|
deriving (Generic, Show)
|
||||||
|
|
||||||
|
instance FromJSON ActionImpl
|
@ -2,10 +2,19 @@ module Wasp.Cli.Command.AI.GenerateNewProject.Common
|
|||||||
( NewProjectDetails (..),
|
( NewProjectDetails (..),
|
||||||
File,
|
File,
|
||||||
AuthProvider (..),
|
AuthProvider (..),
|
||||||
|
queryChatGPTForJSON,
|
||||||
|
defaultChatGPTParams,
|
||||||
|
writeToWaspFileEnd,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
|
import Data.Aeson (FromJSON)
|
||||||
|
import qualified Data.Aeson as Aeson
|
||||||
|
import Data.Maybe (fromMaybe)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
|
import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, queryChatGPT, writeToFile)
|
||||||
|
import Wasp.OpenAI.ChatGPT (ChatGPTParams (..), ChatMessage (..), Model (GPT_3_5_turbo_16k))
|
||||||
|
import Wasp.Util (naiveTrimJSON, textToLazyBS)
|
||||||
|
|
||||||
data NewProjectDetails = NewProjectDetails
|
data NewProjectDetails = NewProjectDetails
|
||||||
{ _projectAppName :: !String,
|
{ _projectAppName :: !String,
|
||||||
@ -13,8 +22,29 @@ data NewProjectDetails = NewProjectDetails
|
|||||||
_projectAuth :: !AuthProvider
|
_projectAuth :: !AuthProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
-- TODO: Make these relative to WaspProjectDir?
|
-- TODO: Make these relative to WaspProjectDir, via StrongPath?
|
||||||
type File = (FilePath, Text)
|
type File = (FilePath, Text)
|
||||||
|
|
||||||
-- TODO: Support more methods.
|
-- TODO: Support more methods.
|
||||||
data AuthProvider = UsernameAndPassword
|
data AuthProvider = UsernameAndPassword
|
||||||
|
|
||||||
|
queryChatGPTForJSON :: FromJSON a => ChatGPTParams -> [ChatMessage] -> CodeAgent a
|
||||||
|
queryChatGPTForJSON chatGPTParams chatMessages = do
|
||||||
|
responseJSONText <- naiveTrimJSON <$> queryChatGPT chatGPTParams chatMessages
|
||||||
|
case Aeson.eitherDecode $ textToLazyBS responseJSONText of
|
||||||
|
Right plan -> return plan
|
||||||
|
Left _errMsg ->
|
||||||
|
-- TODO: Handle this better.
|
||||||
|
-- Try sending response back to chatGPT and ask it to fix it -> hey it is not valid JSON, fix it.
|
||||||
|
-- Write to log, to let user know.
|
||||||
|
error "Failed to parse plan"
|
||||||
|
|
||||||
|
-- TODO: Test more for the optimal temperature (possibly higher).
|
||||||
|
-- TODO: Should we make sure we have max_tokens set to high enough?
|
||||||
|
defaultChatGPTParams :: ChatGPTParams
|
||||||
|
defaultChatGPTParams = ChatGPTParams {_model = GPT_3_5_turbo_16k, _temperature = Just 1.0}
|
||||||
|
|
||||||
|
writeToWaspFileEnd :: FilePath -> Text -> CodeAgent ()
|
||||||
|
writeToWaspFileEnd waspFilePath text = do
|
||||||
|
writeToFile waspFilePath $
|
||||||
|
(<> "\n" <> text) . fromMaybe (error "wasp file shouldn't be empty")
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
module Wasp.Cli.Command.AI.GenerateNewProject.Entity
|
||||||
|
( writeEntitiesToWaspFile,
|
||||||
|
entityPlanToWaspDecl,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import NeatInterpolation (trimming)
|
||||||
|
import Wasp.Cli.Command.AI.CodeAgent (CodeAgent)
|
||||||
|
import Wasp.Cli.Command.AI.GenerateNewProject.Common (writeToWaspFileEnd)
|
||||||
|
import qualified Wasp.Cli.Command.AI.GenerateNewProject.Plan as Plan
|
||||||
|
|
||||||
|
writeEntitiesToWaspFile :: FilePath -> [Plan.Entity] -> CodeAgent ()
|
||||||
|
writeEntitiesToWaspFile waspFilePath entityPlans = do
|
||||||
|
writeToWaspFileEnd waspFilePath $ "\n" <> entitiesCode
|
||||||
|
where
|
||||||
|
entitiesCode = T.intercalate "\n\n" $ entityPlanToWaspDecl <$> entityPlans
|
||||||
|
|
||||||
|
entityPlanToWaspDecl :: Plan.Entity -> Text
|
||||||
|
entityPlanToWaspDecl plan =
|
||||||
|
let name = T.pack $ Plan.entityName plan
|
||||||
|
pslBody = T.pack $ Plan.entityBodyPsl plan
|
||||||
|
in [trimming|
|
||||||
|
entity ${name} {=psl
|
||||||
|
${pslBody}
|
||||||
|
psl=}
|
||||||
|
|]
|
@ -7,38 +7,32 @@ module Wasp.Cli.Command.AI.GenerateNewProject.Plan
|
|||||||
Action (..),
|
Action (..),
|
||||||
Page (..),
|
Page (..),
|
||||||
generatePlan,
|
generatePlan,
|
||||||
|
PlanRule,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import Data.Aeson (FromJSON)
|
import Data.Aeson (FromJSON)
|
||||||
import qualified Data.Aeson as Aeson
|
|
||||||
import Data.ByteString.Lazy.UTF8 (ByteString)
|
|
||||||
import Data.Text (Text)
|
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.Lazy as TL
|
|
||||||
import qualified Data.Text.Lazy.Encoding as TLE
|
|
||||||
import GHC.Generics (Generic)
|
import GHC.Generics (Generic)
|
||||||
import NeatInterpolation (trimming)
|
import NeatInterpolation (trimming)
|
||||||
import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, queryChatGPT)
|
import Wasp.Cli.Command.AI.CodeAgent (CodeAgent)
|
||||||
import Wasp.Cli.Command.AI.GenerateNewProject.Common (NewProjectDetails (_projectAppName, _projectDescription))
|
import Wasp.Cli.Command.AI.GenerateNewProject.Common
|
||||||
|
( AuthProvider (UsernameAndPassword),
|
||||||
|
NewProjectDetails (..),
|
||||||
|
defaultChatGPTParams,
|
||||||
|
queryChatGPTForJSON,
|
||||||
|
)
|
||||||
import Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts (appDescriptionStartMarkerLine)
|
import Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts (appDescriptionStartMarkerLine)
|
||||||
import qualified Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts as Prompts
|
import qualified Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts as Prompts
|
||||||
import Wasp.OpenAI.ChatGPT (ChatGPTParams (..), ChatMessage (..), ChatRole (..), Model (..))
|
import Wasp.OpenAI.ChatGPT (ChatMessage (..), ChatRole (..))
|
||||||
|
|
||||||
generatePlan :: NewProjectDetails -> CodeAgent Plan
|
-- | Additional rule to follow while generating plan.
|
||||||
generatePlan newProjectDetails = do
|
type PlanRule = String
|
||||||
responseJSONText <- naiveTrimJSON <$> queryChatGPT chatGPTParams chatMessages
|
|
||||||
case Aeson.eitherDecode $ textToLazyBS responseJSONText of
|
generatePlan :: NewProjectDetails -> [PlanRule] -> CodeAgent Plan
|
||||||
Right plan -> return plan
|
generatePlan newProjectDetails planRules = do
|
||||||
Left _errMsg ->
|
queryChatGPTForJSON defaultChatGPTParams chatMessages
|
||||||
-- TODO: Handle this better.
|
|
||||||
-- Try sending response back to chatGPT and ask it to fix it -> hey it is not valid JSON, fix it.
|
|
||||||
-- Write to log, to let user know.
|
|
||||||
error "Failed to parse plan"
|
|
||||||
where
|
where
|
||||||
-- TODO: Try configuring temperature.
|
|
||||||
-- TODO: Make sure we have max_tokens set to high enough.
|
|
||||||
chatGPTParams = ChatGPTParams {_model = GPT_3_5_turbo_16k, _temperature = Just 1.0}
|
|
||||||
chatMessages =
|
chatMessages =
|
||||||
[ ChatMessage {role = System, content = Prompts.systemPrompt},
|
[ ChatMessage {role = System, content = Prompts.systemPrompt},
|
||||||
ChatMessage {role = User, content = planPrompt}
|
ChatMessage {role = User, content = planPrompt}
|
||||||
@ -47,6 +41,7 @@ generatePlan newProjectDetails = do
|
|||||||
appDesc = T.pack $ _projectDescription newProjectDetails
|
appDesc = T.pack $ _projectDescription newProjectDetails
|
||||||
basicWaspLangInfoPrompt = Prompts.basicWaspLangInfo
|
basicWaspLangInfoPrompt = Prompts.basicWaspLangInfo
|
||||||
waspFileExamplePrompt = Prompts.waspFileExample
|
waspFileExamplePrompt = Prompts.waspFileExample
|
||||||
|
rulesText = T.pack . unlines $ "Rules:" : map (" - " ++) planRules
|
||||||
planPrompt =
|
planPrompt =
|
||||||
[trimming|
|
[trimming|
|
||||||
${basicWaspLangInfoPrompt}
|
${basicWaspLangInfoPrompt}
|
||||||
@ -80,9 +75,10 @@ generatePlan newProjectDetails = do
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
Make sure to generate at least one page with routePath "/".
|
${rulesText}
|
||||||
|
|
||||||
We will later use this plan to implement all of these parts of Wasp app.
|
We will later use this plan to implement all of these parts of Wasp app,
|
||||||
|
so make sure descriptions are detailed enough to guide implementing them.
|
||||||
|
|
||||||
Please, respond ONLY with a valid JSON that is a plan.
|
Please, respond ONLY with a valid JSON that is a plan.
|
||||||
There should be no other text in the response.
|
There should be no other text in the response.
|
||||||
@ -171,15 +167,3 @@ data Page = Page
|
|||||||
deriving (Generic, Show)
|
deriving (Generic, Show)
|
||||||
|
|
||||||
instance FromJSON Page
|
instance FromJSON Page
|
||||||
|
|
||||||
-- | Given a text containing a single instance of JSON and some text around it but no { or }, trim
|
|
||||||
-- it until just JSON is left.
|
|
||||||
-- Examples
|
|
||||||
-- naiveTrimJson "some text { \"a\": 5 } yay" == "{\"a\": 5 }"
|
|
||||||
-- naiveTrimJson "some {text} { \"a\": 5 }" -> won't work correctly.
|
|
||||||
naiveTrimJSON :: Text -> Text
|
|
||||||
naiveTrimJSON textContainingJson =
|
|
||||||
T.reverse . T.dropWhile (/= '}') . T.reverse . T.dropWhile (/= '{') $ textContainingJson
|
|
||||||
|
|
||||||
textToLazyBS :: Text -> ByteString
|
|
||||||
textToLazyBS = TLE.encodeUtf8 . TL.fromStrict
|
|
||||||
|
@ -10,15 +10,16 @@ import NeatInterpolation (trimming)
|
|||||||
import qualified StrongPath as SP
|
import qualified StrongPath as SP
|
||||||
import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, writeNewFile)
|
import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, writeNewFile)
|
||||||
import Wasp.Cli.Command.AI.GenerateNewProject.Common (AuthProvider (..), File, NewProjectDetails (..))
|
import Wasp.Cli.Command.AI.GenerateNewProject.Common (AuthProvider (..), File, NewProjectDetails (..))
|
||||||
|
import Wasp.Cli.Command.AI.GenerateNewProject.Plan (PlanRule)
|
||||||
import Wasp.Cli.Command.CreateNewProject (readCoreWaspProjectFiles)
|
import Wasp.Cli.Command.CreateNewProject (readCoreWaspProjectFiles)
|
||||||
import qualified Wasp.Version
|
import qualified Wasp.Version
|
||||||
|
|
||||||
generateAndWriteProjectSkeleton :: NewProjectDetails -> CodeAgent FilePath
|
generateAndWriteProjectSkeleton :: NewProjectDetails -> CodeAgent (FilePath, [PlanRule])
|
||||||
generateAndWriteProjectSkeleton newProjectDetails = do
|
generateAndWriteProjectSkeleton newProjectDetails = do
|
||||||
coreFiles <- liftIO $ map (first SP.fromRelFile) <$> readCoreWaspProjectFiles
|
coreFiles <- liftIO $ map (first SP.fromRelFile) <$> readCoreWaspProjectFiles
|
||||||
mapM_ writeNewFile coreFiles
|
mapM_ writeNewFile coreFiles
|
||||||
|
|
||||||
let waspFile@(waspFilePath, _) = generateBaseWaspFile newProjectDetails
|
let (waspFile@(waspFilePath, _), planRules) = generateBaseWaspFile newProjectDetails
|
||||||
writeNewFile waspFile
|
writeNewFile waspFile
|
||||||
|
|
||||||
case _projectAuth newProjectDetails of
|
case _projectAuth newProjectDetails of
|
||||||
@ -28,28 +29,36 @@ generateAndWriteProjectSkeleton newProjectDetails = do
|
|||||||
|
|
||||||
writeNewFile generateDotEnvServerFile
|
writeNewFile generateDotEnvServerFile
|
||||||
|
|
||||||
return waspFilePath
|
return (waspFilePath, planRules)
|
||||||
|
|
||||||
generateBaseWaspFile :: NewProjectDetails -> File
|
generateBaseWaspFile :: NewProjectDetails -> (File, [PlanRule])
|
||||||
generateBaseWaspFile newProjectDetails = (path, content)
|
generateBaseWaspFile newProjectDetails = ((path, content), planRules)
|
||||||
where
|
where
|
||||||
path = "main.wasp"
|
path = "main.wasp"
|
||||||
appName = T.pack $ _projectAppName newProjectDetails
|
appName = T.pack $ _projectAppName newProjectDetails
|
||||||
appTitle = appName
|
appTitle = appName
|
||||||
waspVersion = T.pack $ show Wasp.Version.waspVersion
|
waspVersion = T.pack $ show Wasp.Version.waspVersion
|
||||||
appAuth = case _projectAuth newProjectDetails of
|
(appAuth, authPlanRules) = case _projectAuth newProjectDetails of
|
||||||
-- NOTE: We assume here that there will be a page with route "/".
|
-- NOTE: We assume two things here:
|
||||||
|
-- - that there will be a page with route "/".
|
||||||
|
-- - that there will be a User entity with 'id', 'username' and 'password'.
|
||||||
|
-- We later that those will be added to Plan.
|
||||||
UsernameAndPassword ->
|
UsernameAndPassword ->
|
||||||
[trimming|
|
( [trimming|
|
||||||
auth: {
|
auth: {
|
||||||
userEntity: User,
|
userEntity: User,
|
||||||
methods: {
|
methods: {
|
||||||
usernameAndPassword: {}
|
usernameAndPassword: {}
|
||||||
|
},
|
||||||
|
onAuthFailedRedirectTo: "/login",
|
||||||
|
onAuthSucceededRedirectTo: "/"
|
||||||
},
|
},
|
||||||
onAuthFailedRedirectTo: "/login",
|
|],
|
||||||
onAuthSucceededRedirectTo: "/"
|
[ "Generate a User entity, with at least the following fields: 'id', 'username', 'password'.",
|
||||||
},
|
"There should be a page with route path \"/\"."
|
||||||
|]
|
]
|
||||||
|
)
|
||||||
|
planRules = authPlanRules <> ["Don't generate the Login or Signup page."]
|
||||||
content =
|
content =
|
||||||
[trimming|
|
[trimming|
|
||||||
app ${appName} {
|
app ${appName} {
|
||||||
@ -60,12 +69,6 @@ generateBaseWaspFile newProjectDetails = (path, content)
|
|||||||
${appAuth}
|
${appAuth}
|
||||||
}
|
}
|
||||||
|
|
||||||
entity User {=psl
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
username String @unique
|
|
||||||
password String
|
|
||||||
psl=}
|
|
||||||
|
|
||||||
route LoginRoute { path: "/login", to: LoginPage }
|
route LoginRoute { path: "/login", to: LoginPage }
|
||||||
page LoginPage {
|
page LoginPage {
|
||||||
component: import Login from "@client/Login.jsx"
|
component: import Login from "@client/Login.jsx"
|
||||||
|
@ -33,6 +33,8 @@ module Wasp.Util
|
|||||||
kebabToCamelCase,
|
kebabToCamelCase,
|
||||||
maybeToEither,
|
maybeToEither,
|
||||||
whenM,
|
whenM,
|
||||||
|
naiveTrimJSON,
|
||||||
|
textToLazyBS,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
@ -41,6 +43,7 @@ import Control.Monad (unless, when)
|
|||||||
import qualified Crypto.Hash.SHA256 as SHA256
|
import qualified Crypto.Hash.SHA256 as SHA256
|
||||||
import qualified Data.Aeson as Aeson
|
import qualified Data.Aeson as Aeson
|
||||||
import qualified Data.ByteString as B
|
import qualified Data.ByteString as B
|
||||||
|
import qualified Data.ByteString.Lazy as BSL
|
||||||
import qualified Data.ByteString.UTF8 as BSU
|
import qualified Data.ByteString.UTF8 as BSU
|
||||||
import Data.Char (isSpace, isUpper, toLower, toUpper)
|
import Data.Char (isSpace, isUpper, toLower, toUpper)
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
@ -48,8 +51,11 @@ import Data.List (intercalate)
|
|||||||
import Data.List.Split (splitOn, wordsBy)
|
import Data.List.Split (splitOn, wordsBy)
|
||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import qualified Data.Text.Encoding as TextEncoding
|
import qualified Data.Text.Encoding as TextEncoding
|
||||||
|
import qualified Data.Text.Lazy as TL
|
||||||
|
import qualified Data.Text.Lazy.Encoding as TLE
|
||||||
import StrongPath (File, Path')
|
import StrongPath (File, Path')
|
||||||
import qualified StrongPath as SP
|
import qualified StrongPath as SP
|
||||||
import Text.Printf (printf)
|
import Text.Printf (printf)
|
||||||
@ -230,3 +236,15 @@ maybeToEither leftValue = maybe (Left leftValue) Right
|
|||||||
|
|
||||||
getEnvVarDefinition :: (String, String) -> String
|
getEnvVarDefinition :: (String, String) -> String
|
||||||
getEnvVarDefinition (name, value) = concat [name, "=", value]
|
getEnvVarDefinition (name, value) = concat [name, "=", value]
|
||||||
|
|
||||||
|
-- | Given a text containing a single instance of JSON and some text around it but no { or }, trim
|
||||||
|
-- it until just JSON is left.
|
||||||
|
-- Examples
|
||||||
|
-- naiveTrimJson "some text { \"a\": 5 } yay" == "{\"a\": 5 }"
|
||||||
|
-- naiveTrimJson "some {text} { \"a\": 5 }" -> won't work correctly.
|
||||||
|
naiveTrimJSON :: Text -> Text
|
||||||
|
naiveTrimJSON textContainingJson =
|
||||||
|
T.reverse . T.dropWhile (/= '}') . T.reverse . T.dropWhile (/= '{') $ textContainingJson
|
||||||
|
|
||||||
|
textToLazyBS :: Text -> BSL.ByteString
|
||||||
|
textToLazyBS = TLE.encodeUtf8 . TL.fromStrict
|
||||||
|
@ -372,8 +372,10 @@ library cli-lib
|
|||||||
Wasp.Cli.FileSystem
|
Wasp.Cli.FileSystem
|
||||||
Wasp.Cli.Command.AI.CodeAgent
|
Wasp.Cli.Command.AI.CodeAgent
|
||||||
Wasp.Cli.Command.AI.GenerateNewProject
|
Wasp.Cli.Command.AI.GenerateNewProject
|
||||||
|
Wasp.Cli.Command.AI.GenerateNewProject.Action
|
||||||
Wasp.Cli.Command.AI.GenerateNewProject.Common
|
Wasp.Cli.Command.AI.GenerateNewProject.Common
|
||||||
Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts
|
Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts
|
||||||
|
Wasp.Cli.Command.AI.GenerateNewProject.Entity
|
||||||
Wasp.Cli.Command.AI.GenerateNewProject.Plan
|
Wasp.Cli.Command.AI.GenerateNewProject.Plan
|
||||||
Wasp.Cli.Command.AI.GenerateNewProject.Skeleton
|
Wasp.Cli.Command.AI.GenerateNewProject.Skeleton
|
||||||
Wasp.Cli.Command.AI.New
|
Wasp.Cli.Command.AI.New
|
||||||
|
Loading…
Reference in New Issue
Block a user