mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-23 19:29:17 +03:00
Added plan fixing. (#1296)
This commit is contained in:
parent
349875b242
commit
6c2049ad5a
@ -11,9 +11,12 @@ module Wasp.AI.GenerateNewProject.Plan
|
|||||||
where
|
where
|
||||||
|
|
||||||
import Data.Aeson (FromJSON)
|
import Data.Aeson (FromJSON)
|
||||||
|
import Data.Aeson.Types (ToJSON)
|
||||||
|
import Data.List (find, intercalate, isPrefixOf)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import GHC.Generics (Generic)
|
import GHC.Generics (Generic)
|
||||||
import NeatInterpolation (trimming)
|
import NeatInterpolation (trimming)
|
||||||
|
import qualified Text.Parsec as Parsec
|
||||||
import Wasp.AI.CodeAgent (CodeAgent)
|
import Wasp.AI.CodeAgent (CodeAgent)
|
||||||
import Wasp.AI.GenerateNewProject.Common
|
import Wasp.AI.GenerateNewProject.Common
|
||||||
( NewProjectDetails (..),
|
( NewProjectDetails (..),
|
||||||
@ -23,6 +26,8 @@ import Wasp.AI.GenerateNewProject.Common
|
|||||||
import Wasp.AI.GenerateNewProject.Common.Prompts (appDescriptionStartMarkerLine)
|
import Wasp.AI.GenerateNewProject.Common.Prompts (appDescriptionStartMarkerLine)
|
||||||
import qualified Wasp.AI.GenerateNewProject.Common.Prompts as Prompts
|
import qualified Wasp.AI.GenerateNewProject.Common.Prompts as Prompts
|
||||||
import Wasp.AI.OpenAI.ChatGPT (ChatMessage (..), ChatRole (..))
|
import Wasp.AI.OpenAI.ChatGPT (ChatMessage (..), ChatRole (..))
|
||||||
|
import qualified Wasp.Psl.Parser.Model as Psl.Parser
|
||||||
|
import qualified Wasp.Util.Aeson as Util.Aeson
|
||||||
|
|
||||||
-- | Additional rule to follow while generating plan.
|
-- | Additional rule to follow while generating plan.
|
||||||
type PlanRule = String
|
type PlanRule = String
|
||||||
@ -30,6 +35,7 @@ type PlanRule = String
|
|||||||
generatePlan :: Wasp.AI.GenerateNewProject.Common.NewProjectDetails -> [PlanRule] -> CodeAgent Plan
|
generatePlan :: Wasp.AI.GenerateNewProject.Common.NewProjectDetails -> [PlanRule] -> CodeAgent Plan
|
||||||
generatePlan newProjectDetails planRules = do
|
generatePlan newProjectDetails planRules = do
|
||||||
queryChatGPTForJSON defaultChatGPTParams chatMessages
|
queryChatGPTForJSON defaultChatGPTParams chatMessages
|
||||||
|
>>= fixPlanIfNeeded
|
||||||
where
|
where
|
||||||
chatMessages =
|
chatMessages =
|
||||||
[ ChatMessage {role = System, content = Prompts.systemPrompt},
|
[ ChatMessage {role = System, content = Prompts.systemPrompt},
|
||||||
@ -55,7 +61,7 @@ generatePlan newProjectDetails planRules = do
|
|||||||
{
|
{
|
||||||
"entities": [{
|
"entities": [{
|
||||||
"entityName": "EntityName",
|
"entityName": "EntityName",
|
||||||
"entityBodyPsl": "id Int @id\\nname String"
|
"entityBodyPsl": "id Int @id \n name String"
|
||||||
}],
|
}],
|
||||||
"actions": [{
|
"actions": [{
|
||||||
"opName": "ActionName",
|
"opName": "ActionName",
|
||||||
@ -79,6 +85,9 @@ generatePlan newProjectDetails planRules = do
|
|||||||
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.
|
so make sure descriptions are detailed enough to guide implementing them.
|
||||||
|
|
||||||
|
Typically, plan will have at least one query, at least one action, at least one page, and at
|
||||||
|
least two entities. It will very likely though have more than one of each.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@ -88,6 +97,114 @@ generatePlan newProjectDetails planRules = do
|
|||||||
${appDesc}
|
${appDesc}
|
||||||
|]
|
|]
|
||||||
|
|
||||||
|
fixPlanIfNeeded :: Plan -> CodeAgent Plan
|
||||||
|
fixPlanIfNeeded plan = do
|
||||||
|
let issues =
|
||||||
|
checkPlanForEntityIssues plan
|
||||||
|
<> checkPlanForOperationIssues Query plan
|
||||||
|
<> checkPlanForOperationIssues Action plan
|
||||||
|
<> checkPlanForPageIssues plan
|
||||||
|
if null issues
|
||||||
|
then return plan
|
||||||
|
else do
|
||||||
|
let issuesText = T.pack $ intercalate "\n" $ (<> " - ") <$> issues
|
||||||
|
queryChatGPTForJSON defaultChatGPTParams $
|
||||||
|
chatMessages
|
||||||
|
<> [ ChatMessage {role = Assistant, content = Util.Aeson.encodeToText plan},
|
||||||
|
ChatMessage
|
||||||
|
{ role = User,
|
||||||
|
content =
|
||||||
|
[trimming|
|
||||||
|
I found following potential issues with the plan that you made:
|
||||||
|
|
||||||
|
${issuesText}
|
||||||
|
|
||||||
|
Please improve the plan with regard to these issues and any other potential issues that you find.
|
||||||
|
|
||||||
|
Respond ONLY with a valid JSON that is a plan.
|
||||||
|
There should be no other text or explanations in the response.
|
||||||
|
|]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
checkPlanForEntityIssues :: Plan -> [String]
|
||||||
|
checkPlanForEntityIssues plan =
|
||||||
|
checkNumEntities
|
||||||
|
<> checkUserEntity
|
||||||
|
<> concatMap checkIfEntityPSLCompiles (entities plan)
|
||||||
|
where
|
||||||
|
checkNumEntities =
|
||||||
|
let numEntities = length (entities plan)
|
||||||
|
in if numEntities < 2
|
||||||
|
then
|
||||||
|
[ "There is only " <> show numEntities <> " entities in the plan,"
|
||||||
|
<> " I would expect at least 2 or more."
|
||||||
|
]
|
||||||
|
else []
|
||||||
|
|
||||||
|
checkUserEntity =
|
||||||
|
case find ((== "User") . entityName) (entities plan) of
|
||||||
|
Just _userEntity -> [] -- TODO: I could check here if it contains correct fields.
|
||||||
|
Nothing -> ["'User' entity is missing."]
|
||||||
|
|
||||||
|
checkIfEntityPSLCompiles entity =
|
||||||
|
case parsePslBody (entityBodyPsl entity) of
|
||||||
|
Left parseError ->
|
||||||
|
[ "Failed to parse PSL body of entity '" <> entityName entity <> "': "
|
||||||
|
<> show parseError
|
||||||
|
]
|
||||||
|
Right _ -> []
|
||||||
|
|
||||||
|
parsePslBody = Parsec.parse Psl.Parser.body ""
|
||||||
|
|
||||||
|
checkPlanForOperationIssues :: OperationType -> Plan -> [String]
|
||||||
|
checkPlanForOperationIssues opType plan =
|
||||||
|
checkNumOperations
|
||||||
|
<> concatMap checkOperationFnPath operations
|
||||||
|
where
|
||||||
|
operations = caseOnOpType queries actions $ plan
|
||||||
|
|
||||||
|
checkNumOperations =
|
||||||
|
let numOps = length operations
|
||||||
|
in if numOps < 2
|
||||||
|
then
|
||||||
|
[ "There is only " <> show numOps <> " " <> caseOnOpType "queries" "actions" <> " in the plan,"
|
||||||
|
<> " I would expect at least 1 or more."
|
||||||
|
]
|
||||||
|
else []
|
||||||
|
|
||||||
|
checkOperationFnPath op =
|
||||||
|
if not ("@server" `isPrefixOf` opFnPath op)
|
||||||
|
then
|
||||||
|
[ "fn path of " <> caseOnOpType "query" "action" <> " '" <> opName op
|
||||||
|
<> "' must start with '@server'."
|
||||||
|
]
|
||||||
|
else []
|
||||||
|
|
||||||
|
caseOnOpType :: a -> a -> a
|
||||||
|
caseOnOpType queryCase actionCase = case opType of Query -> queryCase; Action -> actionCase
|
||||||
|
|
||||||
|
checkPlanForPageIssues :: Plan -> [String]
|
||||||
|
checkPlanForPageIssues plan =
|
||||||
|
checkNumPages
|
||||||
|
<> concatMap checkPageComponentPath (pages plan)
|
||||||
|
where
|
||||||
|
checkNumPages =
|
||||||
|
let numPages = length (pages plan)
|
||||||
|
in if numPages < 2
|
||||||
|
then
|
||||||
|
[ "There is only " <> show numPages <> " pages in the plan,"
|
||||||
|
<> " I would expect at least 1 or more."
|
||||||
|
]
|
||||||
|
else []
|
||||||
|
|
||||||
|
checkPageComponentPath page =
|
||||||
|
if not ("@client" `isPrefixOf` componentPath page)
|
||||||
|
then
|
||||||
|
[ "component path of page '" <> pageName page <> "' must start with '@client'."
|
||||||
|
]
|
||||||
|
else []
|
||||||
|
|
||||||
-- TODO: Alternative idea is to give quite more autonomy and equip it with tools (functions) it
|
-- TODO: Alternative idea is to give quite more autonomy and equip it with tools (functions) it
|
||||||
-- needs to build correct context, and then let it drive itself completely on its own. So we give
|
-- needs to build correct context, and then let it drive itself completely on its own. So we give
|
||||||
-- it app description, and then let it just go for it -> it would be making freeform plans for
|
-- it app description, and then let it just go for it -> it would be making freeform plans for
|
||||||
@ -131,6 +248,8 @@ data Plan = Plan
|
|||||||
|
|
||||||
instance FromJSON Plan
|
instance FromJSON Plan
|
||||||
|
|
||||||
|
instance ToJSON Plan
|
||||||
|
|
||||||
data Entity = Entity
|
data Entity = Entity
|
||||||
{ entityName :: String,
|
{ entityName :: String,
|
||||||
entityBodyPsl :: String
|
entityBodyPsl :: String
|
||||||
@ -139,6 +258,10 @@ data Entity = Entity
|
|||||||
|
|
||||||
instance FromJSON Entity
|
instance FromJSON Entity
|
||||||
|
|
||||||
|
instance ToJSON Entity
|
||||||
|
|
||||||
|
data OperationType = Action | Query
|
||||||
|
|
||||||
data Operation = Operation
|
data Operation = Operation
|
||||||
{ opName :: String,
|
{ opName :: String,
|
||||||
opFnPath :: String,
|
opFnPath :: String,
|
||||||
@ -148,6 +271,8 @@ data Operation = Operation
|
|||||||
|
|
||||||
instance FromJSON Operation
|
instance FromJSON Operation
|
||||||
|
|
||||||
|
instance ToJSON Operation
|
||||||
|
|
||||||
data Page = Page
|
data Page = Page
|
||||||
{ pageName :: String,
|
{ pageName :: String,
|
||||||
componentPath :: String,
|
componentPath :: String,
|
||||||
@ -158,3 +283,5 @@ data Page = Page
|
|||||||
deriving (Generic, Show)
|
deriving (Generic, Show)
|
||||||
|
|
||||||
instance FromJSON Page
|
instance FromJSON Page
|
||||||
|
|
||||||
|
instance ToJSON Page
|
||||||
|
13
waspc/src/Wasp/Util/Aeson.hs
Normal file
13
waspc/src/Wasp/Util/Aeson.hs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
module Wasp.Util.Aeson
|
||||||
|
( encodeToText,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Data.Aeson (ToJSON)
|
||||||
|
import Data.Aeson.Text (encodeToTextBuilder)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Data.Text.Lazy (toStrict)
|
||||||
|
import Data.Text.Lazy.Builder (toLazyText)
|
||||||
|
|
||||||
|
encodeToText :: ToJSON a => a -> Text
|
||||||
|
encodeToText = toStrict . toLazyText . encodeToTextBuilder
|
@ -341,6 +341,7 @@ library
|
|||||||
Wasp.SemanticVersion
|
Wasp.SemanticVersion
|
||||||
Wasp.TypeScript
|
Wasp.TypeScript
|
||||||
Wasp.Util
|
Wasp.Util
|
||||||
|
Wasp.Util.Aeson
|
||||||
Wasp.Util.Network.Socket
|
Wasp.Util.Network.Socket
|
||||||
Wasp.Util.Control.Monad
|
Wasp.Util.Control.Monad
|
||||||
Wasp.Util.Fib
|
Wasp.Util.Fib
|
||||||
|
Loading…
Reference in New Issue
Block a user