Now we use GPT4, if available, for plan.

This commit is contained in:
Martin Sosic 2023-06-30 01:49:33 +02:00
parent 83099c3d99
commit 958807f25b
9 changed files with 98 additions and 55 deletions

View File

@ -11,6 +11,8 @@ module Wasp.AI.CodeAgent
getAllFiles,
queryChatGPT,
getTotalTokensUsage,
getOpenAIApiKey,
checkIfGpt4IsAvailable,
)
where
@ -36,7 +38,12 @@ runCodeAgent :: CodeAgentConfig -> CodeAgent a -> IO a
runCodeAgent config codeAgent =
fst <$> (_unCodeAgent codeAgent `runReaderT` config) `runStateT` initialState
where
initialState = CodeAgentState {_files = H.empty, _usage = []}
initialState =
CodeAgentState
{ _files = H.empty,
_usage = [],
_isGpt4Available = Nothing
}
writeToLog :: Text -> CodeAgent ()
writeToLog msg = asks _writeLog >>= \f -> liftIO $ f msg
@ -64,6 +71,19 @@ queryChatGPT params messages = do
modify $ \s -> s {_usage = _usage s <> [ChatGPT.usage chatResponse]}
return $ ChatGPT.getChatResponseContent chatResponse
getOpenAIApiKey :: CodeAgent OpenAIApiKey
getOpenAIApiKey = asks _openAIApiKey
checkIfGpt4IsAvailable :: CodeAgent Bool
checkIfGpt4IsAvailable = do
gets _isGpt4Available >>= \case
Just isAvailable -> pure isAvailable
Nothing -> do
key <- asks _openAIApiKey
isAvailable <- liftIO $ ChatGPT.checkIfGpt4IsAvailable key
modify $ \s -> s {_isGpt4Available = Just isAvailable}
return isAvailable
type NumTokens = Int
-- | Returns total tokens usage: (<num_prompt_tokens>, <num_completion_tokens>).
@ -75,6 +95,7 @@ getTotalTokensUsage = do
return (numPromptTokens, numCompletionTokens)
data CodeAgentState = CodeAgentState
{ _files :: H.HashMap FilePath Text, -- TODO: Name this "cacheFiles" maybe?
_usage :: [ChatGPT.ChatResponseUsage]
{ _files :: !(H.HashMap FilePath Text), -- TODO: Name this "cacheFiles" maybe?
_usage :: ![ChatGPT.ChatResponseUsage],
_isGpt4Available :: !(Maybe Bool)
}

View File

@ -8,6 +8,7 @@ module Wasp.AI.GenerateNewProject.Common
emptyNewProjectConfig,
queryChatGPTForJSON,
defaultChatGPTParams,
defaultChatGPTParamsForFixing,
writeToWaspFileEnd,
)
where
@ -99,7 +100,10 @@ queryChatGPTForJSON chatGPTParams = doQueryForJSON 0
-- TODO: Test more for the optimal temperature (possibly higher).
defaultChatGPTParams :: ChatGPTParams
defaultChatGPTParams = GPT.ChatGPTParams {_model = GPT.GPT_3_5_turbo_16k, _temperature = Just 1.0}
defaultChatGPTParams = GPT.ChatGPTParams {_model = GPT.GPT_3_5_turbo_16k, _temperature = Just 0.7}
defaultChatGPTParamsForFixing :: ChatGPTParams
defaultChatGPTParamsForFixing = GPT.ChatGPTParams {_model = GPT.GPT_3_5_turbo_16k, _temperature = Just 0.5}
writeToWaspFileEnd :: FilePath -> Text -> CodeAgent ()
writeToWaspFileEnd waspFilePath text = do

View File

@ -22,6 +22,7 @@ import Wasp.AI.CodeAgent (CodeAgent, writeToFile, writeToLog)
import Wasp.AI.GenerateNewProject.Common
( NewProjectDetails (..),
defaultChatGPTParams,
defaultChatGPTParamsForFixing,
queryChatGPTForJSON,
writeToWaspFileEnd,
)
@ -123,7 +124,7 @@ generateOperation operationType newProjectDetails entityPlans operationPlan = do
then return operationImpl
else do
let issuesText = T.pack $ intercalate "\n" ((" - " <>) <$> issues)
queryChatGPTForJSON defaultChatGPTParams $
queryChatGPTForJSON defaultChatGPTParamsForFixing $
chatMessages
<> [ ChatMessage {role = Assistant, content = Util.Aeson.encodeToText operationImpl},
ChatMessage
@ -155,7 +156,7 @@ actionDocPrompt =
```wasp
action updateTask {
fn: import { updateTask } from "@server/taskActions.js",
entities: [Task] // Entities that action uses.
entities: [Task] // Entities that action mutates.
}
```

View File

@ -15,7 +15,7 @@ import NeatInterpolation (trimming)
import Wasp.AI.CodeAgent (CodeAgent, getFile, writeToFile)
import Wasp.AI.GenerateNewProject.Common
( NewProjectDetails,
defaultChatGPTParams,
defaultChatGPTParamsForFixing,
queryChatGPTForJSON,
)
import Wasp.AI.GenerateNewProject.Common.Prompts (appDescriptionBlock)
@ -32,7 +32,7 @@ fixOperationsJsFile newProjectDetails waspFilePath opJsFilePath = do
-- with npm dependencies installed, so we skipped it for now.
fixedOpJsFile <-
queryChatGPTForJSON
defaultChatGPTParams
defaultChatGPTParamsForFixing
[ ChatMessage {role = System, content = Prompts.systemPrompt},
ChatMessage {role = User, content = fixOpJsFilePrompt currentWaspFileContent currentOpJsFileContent}
]
@ -83,6 +83,7 @@ fixOperationsJsFile newProjectDetails waspFilePath opJsFilePath = do
With this in mind, generate a new, fixed NodeJS file with operations (${opJsFilePathText}).
Don't do too big changes, like deleting or adding whole functions, focus on smaller things and those listed above.
DO NOT add new queries / actions to the file, or delete existing ones!
Do actual fixes, don't leave comments with "TODO"!
Please respond ONLY with a valid JSON of the format { opJsFileContent: string }.
There should be no other text in your response. Don't wrap content with the "```" code delimiters.

View File

@ -65,20 +65,24 @@ generatePage newProjectDetails entityPlans actions queries pPlan = do
[trimming|
${basicWaspLangInfoPrompt}
===============
${pageDocPrompt}
===============
We are implementing a Wasp app (check bottom for description).
Entities:
Entities in our app:
${entityDecls}
Actions:
Actions in our app:
${actionsInfo}
Queries:
Queries in our app:
${queriesInfo}
You should use Tailwind to style your pages. Example of Tailwind usage:
We use Tailwind to style pages. Example of Tailwind usage:
```jsx
<div className="p-4 bg-slate-50 rounded-lg">
@ -88,6 +92,8 @@ generatePage newProjectDetails entityPlans actions queries pPlan = do
</div>
```
===============
Let's now implement the following Wasp page:
- name: ${pageName}
- component: ${componentPath}
@ -150,6 +156,14 @@ pageDocPrompt =
);
};
```
A bit about importing actions and queries.
If query is named "myQuery", its import should look like `import myQuery from '@wasp/queries/myQuery';`.
It has to be default import, and name of the file is the same as name of the query.
Same rules apply for actions.
One import statement per query/action.
Note: there is no `useMutation` or `useAction` hook to import in Wasp. Actions are to be called directly.
|]
operationInfo :: Operation -> Text

View File

@ -14,7 +14,7 @@ import NeatInterpolation (trimming)
import Wasp.AI.CodeAgent (CodeAgent, getFile, writeToFile)
import Wasp.AI.GenerateNewProject.Common
( NewProjectDetails (..),
defaultChatGPTParams,
defaultChatGPTParamsForFixing,
queryChatGPTForJSON,
)
import Wasp.AI.GenerateNewProject.Common.Prompts (appDescriptionBlock)
@ -28,7 +28,7 @@ fixPageComponent newProjectDetails waspFilePath pageComponentPath = do
currentPageComponentContent <- fromMaybe (error "couldn't find page file to fix") <$> getFile pageComponentPath
fixedPageComponent <-
queryChatGPTForJSON
defaultChatGPTParams
defaultChatGPTParamsForFixing
[ ChatMessage {role = System, content = Prompts.systemPrompt},
ChatMessage {role = User, content = fixPageComponentPrompt currentWaspFileContent currentPageComponentContent}
]
@ -38,45 +38,39 @@ fixPageComponent newProjectDetails waspFilePath pageComponentPath = do
[trimming|
${basicWaspLangInfoPrompt}
===============
${pageDocPrompt}
===============
We are together building a new Wasp app (description at the end of prompt).
Here is our main.wasp file that we generated so far, to provide you with the additional context about the app:
```wasp
${currentWaspFileContent}
```
Here is a React component (${pageComponentPathText}) containing some frontend code
that we generated together earlier:
```js
${currentPageContent}
```
===============
The React component probably has some mistakes: let's fix it!
Some common mistakes to look for:
Strong guidelines for fixing:
- Make sure to use only queries and actions that are defined in the Wasp file (listed below)!
- You should import queries from "@wasp/queries/{queryName}" like
```
import getTask from '@wasp/queries/getTasks';
```
- You should import actions from "@wasp/actions/{actionName}" like
```
import createTask from '@wasp/actions/createTask';
```
- Ensure query and action imports are correct. One import per query / action, default imports,
name of the file same as name of the query.
- Don't use `useAction` or `useMutation`! Use actions directly.
- Use Tailwind CSS to style the page if you didn't.
- Use <Link /> component from "react-router-dom" to link to other pages where needed.
- "TODO" comments or "..." that should be replaced with actual implementation.
Fix these by replacing them with actual implementation.
- Duplicate imports. If there are any, make sure to remove them.
- If there are any duplicate imports, make sure to remove them.
- There might be some invalid JS or JSX syntax -> fix it if there is any.
Here is our main.wasp file, to provide you with the additional context about the app:
```wasp
${currentWaspFileContent}
```
With this in mind, generate a new, fixed React component (${pageComponentPathText}).
Do actual fixes, don't leave comments with "TODO"!
Please respond ONLY with a valid JSON of the format { pageComponentImpl: string }.

View File

@ -17,15 +17,16 @@ import qualified Data.Text as T
import GHC.Generics (Generic)
import NeatInterpolation (trimming)
import qualified Text.Parsec as Parsec
import Wasp.AI.CodeAgent (CodeAgent)
import Wasp.AI.CodeAgent (CodeAgent, checkIfGpt4IsAvailable)
import Wasp.AI.GenerateNewProject.Common
( NewProjectDetails (..),
defaultChatGPTParams,
defaultChatGPTParamsForFixing,
queryChatGPTForJSON,
)
import Wasp.AI.GenerateNewProject.Common.Prompts (appDescriptionBlock)
import qualified Wasp.AI.GenerateNewProject.Common.Prompts as Prompts
import Wasp.AI.OpenAI.ChatGPT (ChatMessage (..), ChatRole (..))
import Wasp.AI.OpenAI.ChatGPT (ChatGPTParams (_model), ChatMessage (..), ChatRole (..), Model (GPT_4))
import qualified Wasp.Psl.Parser.Model as Psl.Parser
import qualified Wasp.Util.Aeson as Util.Aeson
@ -34,7 +35,8 @@ type PlanRule = String
generatePlan :: NewProjectDetails -> [PlanRule] -> CodeAgent Plan
generatePlan newProjectDetails planRules = do
queryChatGPTForJSON defaultChatGPTParams chatMessages
isGpt4Available <- checkIfGpt4IsAvailable
queryChatGPTForJSON (makeGptParams defaultChatGPTParams isGpt4Available) chatMessages
>>= fixPlanIfNeeded
where
chatMessages =
@ -86,12 +88,14 @@ generatePlan newProjectDetails planRules = do
"componentPath": "@client/pages/Task.jsx",
"routeName: "TaskRoute",
"routePath": "/task/:taskId",
"pageDesc": "Diplays a Task with the specified taskId. Allows editing of the Task.",
"pageDesc": "Diplays a Task with the specified taskId. Allows editing of the Task. Uses getTask query and createTask action.",
}]
}
We will later use this plan to write main.wasp file and all the other parts of Wasp app,
so make sure descriptions are detailed enough to guide implementing them.
Also, mention in the descriptions of actions/queries which entities they work with,
and in descriptions of pages mention which actions/queries they use.
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 have more than one of each, though.
@ -104,6 +108,7 @@ generatePlan newProjectDetails planRules = do
fixPlanIfNeeded :: Plan -> CodeAgent Plan
fixPlanIfNeeded plan = do
isGpt4Available <- checkIfGpt4IsAvailable
let issues =
checkPlanForEntityIssues plan
<> checkPlanForOperationIssues Query plan
@ -113,7 +118,7 @@ generatePlan newProjectDetails planRules = do
then return plan
else do
let issuesText = T.pack $ intercalate "\n" ((" - " <>) <$> issues)
queryChatGPTForJSON defaultChatGPTParams $
queryChatGPTForJSON (makeGptParams defaultChatGPTParamsForFixing isGpt4Available) $
chatMessages
<> [ ChatMessage {role = Assistant, content = Util.Aeson.encodeToText plan},
ChatMessage
@ -132,6 +137,10 @@ generatePlan newProjectDetails planRules = do
}
]
makeGptParams :: ChatGPTParams -> Bool -> ChatGPTParams
makeGptParams params isGpt4Available =
if isGpt4Available then params {_model = GPT_4} else params
checkPlanForEntityIssues :: Plan -> [String]
checkPlanForEntityIssues plan =
checkNumEntities

View File

@ -18,7 +18,7 @@ import NeatInterpolation (trimming)
import Wasp.AI.CodeAgent (CodeAgent, getFile, writeToFile)
import Wasp.AI.GenerateNewProject.Common
( NewProjectDetails,
defaultChatGPTParams,
defaultChatGPTParamsForFixing,
queryChatGPTForJSON,
)
import Wasp.AI.GenerateNewProject.Common.Prompts (appDescriptionBlock)
@ -51,7 +51,7 @@ fixWaspFile newProjectDetails waspFilePath plan = do
OnlyIfCompileErrors | null compileErrors -> return $ WaspFile {waspFileContent = wfContent}
_otherwise ->
queryChatGPTForJSON
defaultChatGPTParams
defaultChatGPTParamsForFixing
[ ChatMessage {role = System, content = Prompts.systemPrompt},
ChatMessage {role = User, content = fixWaspFilePrompt wfContent compileErrors}
]

View File

@ -1,3 +1,4 @@
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
module Wasp.AI.OpenAI.ChatGPT
@ -10,12 +11,13 @@ module Wasp.AI.OpenAI.ChatGPT
ChatMessage (..),
ChatRole (..),
getChatResponseContent,
checkIfGpt4IsAvailable,
)
where
import Control.Arrow ()
import Control.Monad (when)
import Data.Aeson ((.=))
import Data.Aeson (FromJSON, ToJSON, (.=))
import qualified Data.Aeson as Aeson
import Data.ByteString.UTF8 as BSU
import Data.Text (Text)
@ -83,6 +85,13 @@ queryChatGPT apiKey params requestMessages = do
getChatResponseContent :: ChatResponse -> Text
getChatResponseContent = content . message . head . choices
checkIfGpt4IsAvailable :: OpenAIApiKey -> IO Bool
checkIfGpt4IsAvailable apiKey = do
let request =
HTTP.setRequestHeader "Authorization" [BSU.fromString $ "Bearer " <> apiKey] $
HTTP.parseRequest_ $ "GET https://api.openai.com/v1/models/" <> show GPT_4
(200 ==) . HTTP.getResponseStatusCode <$> HTTP.httpNoBody request
data ChatGPTParams = ChatGPTParams
{ _model :: !Model,
_temperature :: !(Maybe Float)
@ -107,47 +116,37 @@ data ChatResponse = ChatResponse
choices :: ![ChatResponseChoice],
usage :: !ChatResponseUsage
}
deriving (Generic, Show)
instance Aeson.FromJSON ChatResponse
deriving (Generic, Show, FromJSON)
data ChatResponseUsage = ChatResponseUsage
{ prompt_tokens :: !Int,
completion_tokens :: !Int,
total_tokens :: !Int
}
deriving (Generic, Show)
instance Aeson.FromJSON ChatResponseUsage
deriving (Generic, Show, FromJSON)
data ChatResponseChoice = ChatResponseChoice
{ index :: !Int,
message :: !ChatMessage,
finish_reason :: !String
}
deriving (Generic, Show)
instance Aeson.FromJSON ChatResponseChoice
deriving (Generic, Show, FromJSON)
data ChatMessage = ChatMessage
{ role :: !ChatRole,
content :: !Text
}
deriving (Generic, Show)
instance Aeson.ToJSON ChatMessage
instance Aeson.FromJSON ChatMessage
deriving (Generic, Show, ToJSON, FromJSON)
data ChatRole = User | System | Assistant
deriving (Generic, Show)
instance Aeson.ToJSON ChatRole where
instance ToJSON ChatRole where
toJSON User = "user"
toJSON System = "system"
toJSON Assistant = "assistant"
instance Aeson.FromJSON ChatRole where
instance FromJSON ChatRole where
parseJSON = Aeson.withText "ChatRole" $ \case
"user" -> return User
"system" -> return System