Wasp ai cli mage (#1601)

* Improved Wasp AI CLI experience.

* fix

* Added CLI Usage, polished CLI commands.

* Added choice of temperature.

* Added styling.

* Added more sophisticated, terminal-only styling.

* fix

* Updated Mage to use new Wasp CLI + added some messaging to CLI.

* fix

* Created wasp-ai/README, updated waspc/README.

* Moved wasp-ai/ to mage/.

* Final polishing (docs, readmes, messaging).

* fix

* Added tests for LogMsg.

* Updated versions of test apps to 0.12.

* fix

* fix

* Update mage/README.md

* fix

* fix
This commit is contained in:
Martin Šošić 2023-12-22 15:14:38 +01:00 committed by GitHub
parent 34f6539590
commit 994a58f0dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
120 changed files with 569 additions and 186 deletions

View File

@ -86,6 +86,10 @@ to install Wasp on OSX/Linux/WSL(Win). From there, just follow the instructions
For more details, check out [the docs](https://wasp-lang.dev/docs). For more details, check out [the docs](https://wasp-lang.dev/docs).
# Wasp AI / Mage
Wasp comes with experimental AI code generator to help you kickstart your next Wasp project -> you can use it via `wasp new` in the CLI (choose "AI" option) if you can provide your OpenAI keys or you can do it via our [Mage web app](https://usemage.ai) in which case our OpenAI keys are used in the background.
# This repository # This repository
This is the main repo of the Wasp universe, containing core code (mostly `waspc` - Wasp compiler) and the supporting materials. This is the main repo of the Wasp universe, containing core code (mostly `waspc` - Wasp compiler) and the supporting materials.

View File

@ -9,3 +9,6 @@
# or modify/delete these two lines. # or modify/delete these two lines.
*.env *.env
*.env.* *.env.*
# Emacs specific
.projectile

View File

@ -33,7 +33,7 @@ RUN cd server && PRISMA_CLIENT_OUTPUT_DIR=../server/node_modules/.prisma/client/
RUN cd server && npm run build RUN cd server && npm run build
FROM base AS server-production FROM base AS server-production
RUN curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s -- -v 0.11.4-wasp-ai-12 RUN curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s -- -v 0.12.0
ENV PATH "$PATH:/root/.local/bin" ENV PATH "$PATH:/root/.local/bin"
ENV NODE_ENV production ENV NODE_ENV production
WORKDIR /app WORKDIR /app

44
mage/README.md Normal file
View File

@ -0,0 +1,44 @@
# Mage
This directory contains the source code of Mage (aka "GPT Web App Generator" aka "Wasp AI"): a Wasp app (so a full-stack web app) that allows you to create a new Wasp app (inception :)!) from just a short description. It uses ChatGPT in a smart way to accomplish this (so it would be clasified as an AI code agent).
Mage is hosted at https://usemage.ai and you can use it there for free.
You can learn more about it [here](https://wasp-lang.dev/blog/2023/07/10/gpt-web-app-generator) and [here](https://wasp-lang.dev/blog/2023/07/17/how-we-built-gpt-web-app-generator).
## Running locally
Mage is really just a client / UI for calling "Wasp AI", which is AI logic that does all the heavy lifting, and is integrated into Wasp's CLI: `wasp`.
So, if you want to generate Wasp apps via AI locally, on your machine, with your OpenAI keys and your choice of models/parameters, we recommend NOT running the Mage app locally, because it is not so easy, instead we recommend you to do it directly via `wasp` CLI, with `wasp new` or `wasp new:ai` commands. Check our docs on how to install `wasp` CLI: https://wasp-lang.dev/docs/quick-start#installation .
If you still want to run Mage web app locally for some specific reason, most likely because you want to contribute, you will need to do the following:
1. Copy `.env.server.example` into `.env.server` and fill in the missing values. You will basically need to provide Github and Google OAuth creds (and first create OAuth apps on both Github and Google if you don't have them yet - if you are a member of Wasp team ask for dev creds, if not you will have to create your own OAuth apps).
2. Run `wasp db start` and then `wasp start`! It might ask you to do `wasp db migrate-dev`, do that if needed.
3. When running Mage locally, it will be looking for `wasp-cli` binary on your machine to use. To satisfy this requirement, you can go to `waspc/` dir (just next to this one) and run `./run install` there. You will want to check though if that Wasp version matches the version Mage expects (check its Dockerfile to see which version of Wasp it expects).
If building `waspc/` is too complex for you (you don't have Haskell toolchain set up, taking too long, ...), you can go into the code of Mage, find where it calls `wasp-cli` and modify that temporarily to call `wasp` instead.
## Developing
### Updating Wasp version in Dockerfile
Keep in mind that Mage, when deployed, will install the version of Wasp specified in its Dockerfile.
So, make sure to update that version to be in sync with the version of Wasp that it was developed against.
Most often that should be the current version of Wasp on `main`, even if not released yet.
## Deployment
Mage is currently deployed on Wasp's Fly.io cloud.
Same as the rest of Wasp (blog/docs, CLI, ...), the latest deployed version is tracked on `release` branch.
So if you want to deploy new version of Mage, you should get it in wanted state on `release` branch, and then deploy from there.
Also, before deploying, check that version of `wasp` in `Dockerfile` makes sense.
To deploy it, just run `wasp deploy fly deploy`. You might want to add `--org wasp` if needed.
## FAQ
Q: What is the difference between Wasp AI and Mage? Are those the same thing?
A: When we say "Wasp AI" we refer to logic implemented in `wasp` CLI, while when we say "Mage" we refer to the Mage web app that really serves as a client for "Wasp AI" (calls it in the background). That said, we sometimes use these interchangeably.

View File

@ -50,7 +50,7 @@ export function RootComponent({ children }) {
cursor-pointer flex-row cursor-pointer flex-row
space-x-3 space-x-3
text-white bg-gradient-to-r from-pink-400 to-amber-400" text-white bg-gradient-to-r from-pink-400 to-amber-400"
onClick={() => window.open("https://github.com/wasp-lang/wasp/tree/wasp-ai")} onClick={() => window.open("https://github.com/wasp-lang/wasp")}
> >
<div <div
className={` className={`
@ -103,7 +103,7 @@ export function RootComponent({ children }) {
<p className="text-center text-slate-500 text-sm mt-2"> <p className="text-center text-slate-500 text-sm mt-2">
This whole app is open-source, you can find the code{" "} This whole app is open-source, you can find the code{" "}
<a <a
href="https://github.com/wasp-lang/wasp/tree/wasp-ai/wasp-ai" href="https://github.com/wasp-lang/wasp/tree/main/mage"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-sky-500 hover:text-sky-600" className="text-sky-500 hover:text-sky-600"

View File

@ -4,8 +4,8 @@ import { FiChevronDown, FiChevronRight } from 'react-icons/fi'
function l(title, overrideTitle) { function l(title, overrideTitle) {
const links = { const links = {
"Wasp": "https://wasp-lang.dev/", "Wasp": "https://wasp-lang.dev/",
"web app": "https://github.com/wasp-lang/wasp/tree/wasp-ai/wasp-ai", "web app": "https://github.com/wasp-lang/wasp/tree/main/mage",
"GPT code agent": "https://github.com/wasp-lang/wasp/tree/wasp-ai/waspc/src/Wasp/AI", "GPT code agent": "https://github.com/wasp-lang/wasp/tree/main/waspc/src/Wasp/AI",
"blog post": "https://wasp-lang.dev/blog/2023/07/10/gpt-web-app-generator" "blog post": "https://wasp-lang.dev/blog/2023/07/10/gpt-web-app-generator"
}; };
@ -46,7 +46,7 @@ const faqs = [
<br/><br/> <br/><br/>
With GPT4 increasing its availability and with LLMs improving in general, the quality of generated code will only get better! With LLMs improving in general, the quality of generated code will only get better!
</p> </p>
}, },
{ {
@ -80,26 +80,28 @@ const faqs = [
<br/><br/> <br/><br/>
However, in the future, when GPT4 becomes cheaper / more available, it would make sense to switch to it completely, since it does generate better code! However, in the future, when GPT4 becomes cheaper / faster, it would make sense to switch to it completely, since it does generate better code!
</p> </p>
}, },
{ {
question: '[Advanced] Can I use GPT4 for the whole app?', question: '[Advanced] Can I use GPT4 for the whole app? / Can I run Mage locally?',
answer: <p> answer: <p>
As mentioned above, we use GPT4 + GPT3.5 for practical reasons, even though using GPT4 exclusively does give better results.<br/> As mentioned above, we use GPT4 + GPT3.5 for practical reasons, even though using GPT4 exclusively does give better results.<br/>
<br/> <br/>
However, if you have access yourself to the OpenAI API, you can use GPT4 for the whole app, or play with adjusting the temperature, by running the Wasp GPT code agent locally!<br/> However, if you have access yourself to the OpenAI API, you can use GPT4 for the whole app, or play with adjusting the temperature, by running the Wasp GPT code agent locally! So same thing like Mage, but via CLI.<br/>
Note: generating an app usually consumes from 20k to 50k tokens, which is then approximately $1 to $2 per app with the current GPT4 pricing (Jul 11th 2023).<br/> Note: generating an app usually consumes from 20k to 50k tokens, which is then approximately $1 to $2 per app with the current GPT4 pricing (Jul 11th 2023).<br/>
<br/> <br/>
You will need to install special version of Wasp:<br/> To run Wasp AI (Mage) locally, make sure you have wasp {'>='}v0.12 installed and just run:<br/>
<span className="bg-slate-800 text-slate-200 p-1 rounded"> <span className="bg-slate-800 text-slate-200 p-1 rounded">
curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s -- -v 0.11.1-wasp-ai-11 wasp new
</span><br/> </span><br/>
When asked, choose AI generation, answer some questions, and your app will start generating!<br/>
<br/> <br/>
Now you can run app generation locally via:<br/> There is also a command for running the same thing programmatically, without interactive questions:<br/>
<span className="bg-slate-800 text-slate-200 p-1 rounded"> <span className="bg-slate-800 text-slate-200 p-1 rounded">
wasp new-ai:disk MyAwesomeApp "Description of my awesome app." {'"{ \\"defaultGptModel\\": \\"gpt-4\\" }"'} wasp new:ai
</span> </span><br/>
Run it with no arguments (as above) to see its usage instructions.
</p> </p>
}, },
] ]

View File

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

View File

@ -278,7 +278,7 @@ export const ResultPage = () => {
> >
<span <span
className="item-center flex gap-2 p-1 px-2 cursor-pointer text-pink-800 bg-pink-200 rounded" className="item-center flex gap-2 p-1 px-2 cursor-pointer text-pink-800 bg-pink-200 rounded"
onClick={() => window.open("https://github.com/wasp-lang/wasp/tree/wasp-ai")} onClick={() => window.open("https://github.com/wasp-lang/wasp")}
> >
<span> <span>
🔮 This is a Wasp powered project. If you like it,{" "} 🔮 This is a Wasp powered project. If you like it,{" "}

View File

Before

Width:  |  Height:  |  Size: 659 KiB

After

Width:  |  Height:  |  Size: 659 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 725 KiB

After

Width:  |  Height:  |  Size: 725 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -62,7 +62,8 @@ export const generateApp: GenerateAppJob<
const stdoutMutex = new Mutex(); const stdoutMutex = new Mutex();
let waspCliProcess = null; let waspCliProcess = null;
const waspCliProcessArgs = [ const waspCliProcessArgs = [
"new-ai", "new:ai",
"--stdout",
project.name, project.name,
project.description, project.description,
JSON.stringify(projectConfig), JSON.stringify(projectConfig),

View File

@ -1,6 +1,6 @@
# Changelog # Changelog
## 0.12.0 ## [WIP] 0.12.0
### ⚠️ Breaking changes ### ⚠️ Breaking changes

View File

@ -326,23 +326,24 @@ NOTE: When you run it for the first time it might take a while (~10 minutes) for
We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.2/) convention when creating commits. We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.2/) convention when creating commits.
## Branching and merging strategy ## Branching and merging strategy
This repo contains both the source code that makes up a Wasp release (under `waspc`), as well as our website containing documentation and blog posts (under `web`). In order to facilitate the development of Wasp code while still allowing for website updates or hotfixes of the current release, we have decided on the following minimal branching strategy. This repo contains both the source code that makes up a Wasp release (under `waspc`), as well as our website containing documentation and blog posts (under `web`), and also Mage web app (under `mage`). In order to facilitate the development of Wasp code while still allowing for website / Mage updates or hotfixes of the current release, we have decided on the following minimal branching strategy.
All Wasp development should be done on feature branches. They form the basis of PRs that will target one of the two following branches: All Wasp development should be done on feature branches. They form the basis of PRs that will target one of the two following branches:
- `main`: this branch contains all the actively developed new features and corresponding documentation updates. Some of these things may not yet be released, but anything merged into `main` should be in a release-ready state. - `main`: this branch contains all the actively developed new features and corresponding documentation updates. Some of these things may not yet be released, but anything merged into `main` should be in a release-ready state.
- This is the default branch to target for any Wasp feature branches. - This is the default branch to target for any Wasp feature branches.
- `release`: this branch contains the source code of current/latest Wasp release, as well as the documentation and blog posts currently published and therefore visible on the website. - `release`: this branch contains the source code of current/latest Wasp release, as well as the documentation and blog posts currently published and therefore visible on the website, and also currently published version of Mage.
- When doing a full release, which means making a new release based on what we have currently on `main`, we do the following: - When doing a full release, which means making a new release based on what we have currently on `main`, we do the following:
1. Update `main` branch by merging `release` into it. There might be conflicts but they shouldn't be too hard to fix. Once `main` is updated, you can create a new waspc release from it, as well as deploy the website from it. 1. Update `main` branch by merging `release` into it. There might be conflicts but they shouldn't be too hard to fix. Once `main` is updated, you can create a new waspc release from it, as well as deploy the website from it.
2. Update `release` branch to this new `main` by merging `main` into it. There will be no conflicts since we already resolved all of them in the previous step. 2. Update `release` branch to this new `main` by merging `main` into it. There will be no conflicts since we already resolved all of them in the previous step.
How do I know where I want to target my PR, to `release` or `main`? How do I know where I want to target my PR, to `release` or `main`?
- If you have a change that you want to publish right now or very soon, certainly earlier than waiting till `main` is ready for publishing, then you want to target `release`. This could be website content update, new blog post, documentation (hot)fix, compiler hotfix that we need to release quickly via a new patch version, ... . - If you have a change that you want to publish right now or very soon, certainly earlier than waiting till `main` is ready for publishing, then you want to target `release`. This could be website content update, new blog post, documentation (hot)fix, compiler hotfix that we need to release quickly via a new patch version, update for Mage that needs to go out now, ... .
- If you have a change that is not urgent and can wait until the next "normal" Wasp release is published, then target `main`. These are new features, refactorings, docs accompanying new features, ... . - If you have a change that is not urgent and can wait until the next "normal" Wasp release is published, then target `main`. These are new features, refactorings, docs accompanying new features, ... .
- Stuff published on `release` (docs, Mage) uses/references version of `wasp` that was last released (so one that is also on `release`).
- TLDR; - TLDR;
- `release` is for changes to the already published stuff (the present). - `release` represents the present, and is for changes to the already published stuff.
- `main` is for changes to the to-be-published stuff (the future). - `main` represents near future, and is for changes to the to-be-published stuff.
## Deployment / CI ## Deployment / CI
We use Github Actions for CI. We use Github Actions for CI.
@ -375,6 +376,7 @@ If it happens just once every so it is probably nothing to worry about. If it ha
- Publish the draft release when ready. - Publish the draft release when ready.
- Merge `release` back into `main` (`git merge release` while on the `main` branch), if needed. - Merge `release` back into `main` (`git merge release` while on the `main` branch), if needed.
- Publish new [docs](/web#deployment) from the `release` branch as well. - Publish new [docs](/web#deployment) from the `release` branch as well.
- Publish new [Mage](/mage#deployment) from the `release` branch as well, if needed.
- Announce new release in Discord. - Announce new release in Discord.
#### Determining next version #### Determining next version
@ -393,7 +395,10 @@ If doing this, steps are the following:
## Documentation ## Documentation
External documentation, for users of Wasp, is hosted at https://wasp-lang.dev/docs, and its source is available at [web/docs](/web/docs), next to the website and blog. External documentation, for users of Wasp, is hosted at https://wasp-lang.dev/docs, and its source is available at [web/docs](/web/docs), next to the website and blog.
## Mage
Wasp's magic GPT web app generator aka Wasp AI aka Mage is hosted at https://usemage.ai and its source is available at [mage](/mage).
Make sure to update it when changes modify how Wasp works. Make sure to update it when changes modify how Wasp works.

View File

@ -46,13 +46,7 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do
args <- getArgs args <- getArgs
let commandCall = case args of let commandCall = case args of
("new" : newArgs) -> Command.Call.New newArgs ("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 ("new:ai" : newAiArgs) -> Command.Call.NewAi newAiArgs
-- web app), while new-ai:disk is useful for us for testing.
[newAiCmd, projectName, appDescription, projectConfigJson]
| newAiCmd `elem` ["new-ai", "new-ai:stdout"] ->
Command.Call.NewAiToStdout projectName appDescription projectConfigJson
| newAiCmd == "new-ai:disk" ->
Command.Call.NewAiToDisk projectName appDescription projectConfigJson
["start"] -> Command.Call.Start ["start"] -> Command.Call.Start
["start", "db"] -> Command.Call.StartDb ["start", "db"] -> Command.Call.StartDb
["clean"] -> Command.Call.Clean ["clean"] -> Command.Call.Clean
@ -88,10 +82,20 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do
case commandCall of case commandCall of
Command.Call.New newArgs -> runCommand $ createNewProject newArgs Command.Call.New newArgs -> runCommand $ createNewProject newArgs
Command.Call.NewAiToStdout projectName appDescription projectConfigJson -> Command.Call.NewAi newAiArgs -> case newAiArgs of
runCommand $ Command.CreateNewProject.AI.createNewProjectNonInteractiveToStdout projectName appDescription projectConfigJson ["--stdout", projectName, appDescription, projectConfigJson] ->
Command.Call.NewAiToDisk projectName appDescription projectConfigJson -> runCommand $
runCommand $ Command.CreateNewProject.AI.createNewProjectNonInteractiveOnDisk projectName appDescription projectConfigJson Command.CreateNewProject.AI.createNewProjectNonInteractiveToStdout
projectName
appDescription
projectConfigJson
[projectName, appDescription, projectConfigJson] ->
runCommand $
Command.CreateNewProject.AI.createNewProjectNonInteractiveOnDisk
projectName
appDescription
projectConfigJson
_ -> printWaspNewAiUsage
Command.Call.Start -> runCommand start Command.Call.Start -> runCommand start
Command.Call.StartDb -> runCommand Command.Start.Db.start Command.Call.StartDb -> runCommand Command.Start.Db.start
Command.Call.Clean -> runCommand clean Command.Call.Clean -> runCommand clean
@ -141,6 +145,11 @@ printUsage =
" -t|--template <template-name>", " -t|--template <template-name>",
" Check out the templates list here: https://github.com/wasp-lang/starters", " Check out the templates list here: https://github.com/wasp-lang/starters",
"", "",
cmd " new:ai <app-name> <app-description> [<config-json>]",
" Uses AI to create a new Wasp project just based on the app name and the description.",
" You can do the same thing with `wasp new` interactively.",
" Run `wasp new:ai` for more info.",
"",
cmd " version Prints current version of CLI.", cmd " version Prints current version of CLI.",
cmd " waspls Run Wasp Language Server. Add --help to get more info.", cmd " waspls Run Wasp Language Server. Add --help to get more info.",
cmd " completion Prints help on bash completion.", cmd " completion Prints help on bash completion.",
@ -230,6 +239,30 @@ printDbUsage =
] ]
{- ORMOLU_ENABLE -} {- ORMOLU_ENABLE -}
{- ORMOLU_DISABLE -}
printWaspNewAiUsage :: IO ()
printWaspNewAiUsage =
putStrLn $
unlines
[ title "USAGE",
" wasp new:ai <app-name> <app-description> <config-json>",
"",
" Config JSON:",
" It is used to provide additional configuration to Wasp AI.",
" Following fields are supported:",
" {",
" \"defaultGptTemperature\"?: number (from 0 to 2)",
" \"planningGptModel\"?: string (OpenAI model name)",
" \"codingGptModel\"?: string (OpenAI model name)",
" \"primaryColor\"?: string (Tailwind color name)",
" }",
"",
title "EXAMPLES",
" wasp new:ai ButtonApp \"One page with button\" \"{}\"",
" wasp new:ai ButtonApp \"One page with button\" \"{ \\\"defaultGptTemperature\\\": 0.5, \\\"codingGptModel\\\": \\\"gpt-4-1106-preview\\\" }\""
]
{- ORMOLU_ENABLE -}
cmd :: String -> String cmd :: String -> String
cmd = mapFirstWord (Term.applyStyles [Term.Yellow, Term.Bold]) cmd = mapFirstWord (Term.applyStyles [Term.Yellow, Term.Bold])

View File

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

View File

@ -5,13 +5,11 @@ where
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import Data.Function ((&)) import Data.Function ((&))
import StrongPath (Abs, Dir, Path')
import qualified StrongPath as SP
import Wasp.Cli.Command (Command) import Wasp.Cli.Command (Command)
import Wasp.Cli.Command.Call (Arguments) import Wasp.Cli.Command.Call (Arguments)
import Wasp.Cli.Command.CreateNewProject.AI (createNewProjectInteractiveOnDisk) import qualified Wasp.Cli.Command.CreateNewProject.AI as AI
import Wasp.Cli.Command.CreateNewProject.ArgumentsParser (parseNewProjectArgs) import Wasp.Cli.Command.CreateNewProject.ArgumentsParser (parseNewProjectArgs)
import Wasp.Cli.Command.CreateNewProject.Common (throwProjectCreationError) import Wasp.Cli.Command.CreateNewProject.Common (printGettingStartedInstructions, throwProjectCreationError)
import Wasp.Cli.Command.CreateNewProject.ProjectDescription import Wasp.Cli.Command.CreateNewProject.ProjectDescription
( NewProjectDescription (..), ( NewProjectDescription (..),
obtainNewProjectDescription, obtainNewProjectDescription,
@ -24,9 +22,7 @@ import Wasp.Cli.Command.CreateNewProject.StarterTemplates
import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Local (createProjectOnDiskFromLocalTemplate) import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Local (createProjectOnDiskFromLocalTemplate)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote (createProjectOnDiskFromRemoteTemplate) import Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote (createProjectOnDiskFromRemoteTemplate)
import Wasp.Cli.Command.Message (cliSendMessageC) import Wasp.Cli.Command.Message (cliSendMessageC)
import Wasp.Cli.Common (WaspProjectDir)
import qualified Wasp.Message as Msg import qualified Wasp.Message as Msg
import qualified Wasp.Util.Terminal as Term
-- | It receives all of the arguments that were passed to the `wasp new` command. -- | It receives all of the arguments that were passed to the `wasp new` command.
createNewProject :: Arguments -> Command () createNewProject :: Arguments -> Command ()
@ -38,19 +34,6 @@ createNewProject args = do
createProjectOnDisk newProjectDescription createProjectOnDisk newProjectDescription
liftIO $ printGettingStartedInstructions $ _absWaspProjectDir newProjectDescription liftIO $ printGettingStartedInstructions $ _absWaspProjectDir newProjectDescription
where
-- This function assumes that the project dir is created inside the current working directory when it
-- prints the instructions.
printGettingStartedInstructions :: Path' Abs (Dir WaspProjectDir) -> IO ()
printGettingStartedInstructions absProjectDir = do
let projectFolder = init . SP.toFilePath . SP.basename $ absProjectDir
{- ORMOLU_DISABLE -}
putStrLn $ Term.applyStyles [Term.Green] $ "Created new Wasp app in ./" ++ projectFolder ++ " directory!"
putStrLn "To run it, do:"
putStrLn ""
putStrLn $ Term.applyStyles [Term.Bold] $ " cd " ++ projectFolder
putStrLn $ Term.applyStyles [Term.Bold] " wasp start"
{- ORMOLU_ENABLE -}
createProjectOnDisk :: NewProjectDescription -> Command () createProjectOnDisk :: NewProjectDescription -> Command ()
createProjectOnDisk createProjectOnDisk
@ -67,4 +50,4 @@ createProjectOnDisk
LocalStarterTemplate metadata -> LocalStarterTemplate metadata ->
liftIO $ createProjectOnDiskFromLocalTemplate absWaspProjectDir projectName appName $ _path metadata liftIO $ createProjectOnDiskFromLocalTemplate absWaspProjectDir projectName appName $ _path metadata
AiGeneratedStarterTemplate -> AiGeneratedStarterTemplate ->
createNewProjectInteractiveOnDisk absWaspProjectDir appName AI.createNewProjectInteractiveOnDisk absWaspProjectDir appName

View File

@ -6,32 +6,110 @@ module Wasp.Cli.Command.CreateNewProject.AI
where where
import Control.Arrow () import Control.Arrow ()
import Control.Monad (unless)
import Control.Monad.Except (MonadError (throwError), MonadIO (liftIO)) import Control.Monad.Except (MonadError (throwError), MonadIO (liftIO))
import Data.Function ((&)) import Data.Function ((&))
import Data.Functor ((<&>))
import Data.List (intercalate)
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.IO as T.IO import qualified Data.Text.IO as T.IO
import StrongPath (Abs, Dir, Path', fromAbsDir) import StrongPath (Abs, Dir, Path', basename, fromAbsDir, fromRelDir)
import StrongPath.Operations () import StrongPath.Operations ()
import System.Directory (createDirectory, createDirectoryIfMissing, setCurrentDirectory) import System.Directory (createDirectory, createDirectoryIfMissing, setCurrentDirectory)
import System.Environment (lookupEnv) import System.Environment (lookupEnv)
import System.FilePath (takeDirectory) import System.FilePath (takeDirectory)
import qualified System.FilePath as FP
import System.IO (hFlush, stdout) import System.IO (hFlush, stdout)
import qualified Wasp.AI.CodeAgent as CA import qualified Wasp.AI.CodeAgent as CA
import qualified Wasp.AI.GenerateNewProject as GNP import qualified Wasp.AI.GenerateNewProject as GNP
import Wasp.AI.GenerateNewProject.Common (NewProjectConfig, NewProjectDetails (..), emptyNewProjectConfig) import Wasp.AI.GenerateNewProject.Common
( NewProjectConfig,
NewProjectDetails (..),
emptyNewProjectConfig,
)
import qualified Wasp.AI.GenerateNewProject.Common as GNP.C
import qualified Wasp.AI.GenerateNewProject.LogMsg as GNP.L
import Wasp.AI.OpenAI (OpenAIApiKey) import Wasp.AI.OpenAI (OpenAIApiKey)
import qualified Wasp.AI.OpenAI.ChatGPT as ChatGPT
import Wasp.Cli.Command (Command, CommandError (CommandError)) import Wasp.Cli.Command (Command, CommandError (CommandError))
import Wasp.Cli.Command.CreateNewProject.ProjectDescription (NewProjectAppName (..), obtainAvailableProjectDirPath, parseWaspProjectNameIntoAppName) import Wasp.Cli.Command.CreateNewProject.ProjectDescription
( NewProjectAppName (..),
obtainAvailableProjectDirPath,
parseWaspProjectNameIntoAppName,
)
import Wasp.Cli.Command.CreateNewProject.StarterTemplates (readWaspProjectSkeletonFiles) import Wasp.Cli.Command.CreateNewProject.StarterTemplates (readWaspProjectSkeletonFiles)
import Wasp.Cli.Common (WaspProjectDir) import Wasp.Cli.Common (WaspProjectDir)
import qualified Wasp.Cli.Interactive as Interactive import qualified Wasp.Cli.Interactive as Interactive
import qualified Wasp.Util as U
import qualified Wasp.Util.Aeson as Utils.Aeson import qualified Wasp.Util.Aeson as Utils.Aeson
import qualified Wasp.Util.Terminal as T
createNewProjectInteractiveOnDisk :: Path' Abs (Dir WaspProjectDir) -> NewProjectAppName -> Command () createNewProjectInteractiveOnDisk :: Path' Abs (Dir WaspProjectDir) -> NewProjectAppName -> Command ()
createNewProjectInteractiveOnDisk waspProjectDir appName = do createNewProjectInteractiveOnDisk waspProjectDir appName = do
openAIApiKey <- getOpenAIApiKey openAIApiKey <- getOpenAIApiKey
appDescription <- liftIO $ Interactive.askForRequiredInput "Describe your app in a couple of sentences" appDescription <- liftIO $ Interactive.askForRequiredInput "Describe your app in a couple of sentences"
liftIO $ createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription emptyNewProjectConfig (planningGptModel, codingGptModel) <-
liftIO $
Interactive.askToChoose'
"Choose GPT model(s) you want to use:"
$ NE.fromList
[ Interactive.Option
"gpt-4 (planning) + gpt-3.5-turbo (coding)"
(Just "Ok results. Cheap and fast. Best cost/benefit ratio.")
(ChatGPT.GPT_4, ChatGPT.GPT_3_5_turbo),
Interactive.Option
"gpt-4 (planning) + gpt-4-1106-preview (coding)"
(Just "Possibly better results, but somewhat slower and somewhat more expensive (~2-3x).")
(ChatGPT.GPT_4, ChatGPT.GPT_4_1106_Preview),
Interactive.Option
"gpt-4 (planning + coding)"
(Just "Best results, but quite slower and quite more expensive (~5x).")
(ChatGPT.GPT_4, ChatGPT.GPT_4)
]
temperature <-
liftIO $
Interactive.askToChoose'
"Choose the creativity level (temperature):"
$ NE.fromList
[ Interactive.Option
"Balanced (0.7)"
(Just "Optimal trade-off between creativity and possible mistakes.")
0.7,
Interactive.Option
"Conventional (0.4)"
(Just "Generates sensible code with minimal amount of mistakes.")
0.4,
Interactive.Option
"Creative (1.0)"
(Just "Generates more creative code, but mistakes are more likely.")
1.0
]
let projectConfig =
emptyNewProjectConfig
{ GNP.C.projectPlanningGptModel = Just planningGptModel,
GNP.C.projectCodingGptModel = Just codingGptModel,
GNP.C.projectDefaultGptTemperature = Just temperature
}
liftIO $ createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription projectConfig
liftIO $ do
putStrLn $
unlines
[ "",
"========",
"",
"⚠️ Experimental tech",
"Since this is a GPT generated app, it will likely contain some mistakes, proportional to how",
"complex the app is. If there are some in your app, check out Wasp docs for help while",
"fixing them, and also feel free to reach out to us on Discord! You can also try",
"generating the app again to get different results (try playing with the creativity level).",
" - Wasp docs: https://wasp-lang.dev/docs",
" - Our Discord: https://discord.gg/rzdnErX",
"",
"========"
]
createNewProjectNonInteractiveOnDisk :: String -> String -> String -> Command () createNewProjectNonInteractiveOnDisk :: String -> String -> String -> Command ()
createNewProjectNonInteractiveOnDisk projectName appDescription projectConfigJson = do createNewProjectNonInteractiveOnDisk projectName appDescription projectConfigJson = do
@ -64,14 +142,16 @@ createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription projec
CA._writeLog = forwardLogToStdout CA._writeLog = forwardLogToStdout
} }
writeFileToDisk :: FilePath -> T.Text -> IO ()
writeFileToDisk path content = do writeFileToDisk path content = do
createDirectoryIfMissing True (takeDirectory path) createDirectoryIfMissing True (takeDirectory path)
T.IO.writeFile path content T.IO.writeFile path content
putStrLn $ "> Wrote file at " <> path putStrLn $ T.applyStyles [T.Yellow] $ "> Wrote to file: " <> fromRelDir (basename waspProjectDir) FP.</> path
hFlush stdout hFlush stdout
forwardLogToStdout :: GNP.L.LogMsg -> IO ()
forwardLogToStdout msg = do forwardLogToStdout msg = do
putStrLn . T.unpack $ msg putStrLn $ GNP.L.toTermString msg
hFlush stdout hFlush stdout
-- | Instead of writing files to disk, it will write files (and logs) to the stdout, -- | Instead of writing files to disk, it will write files (and logs) to the stdout,
@ -97,25 +177,32 @@ createNewProjectNonInteractiveToStdout projectName appDescription projectConfigJ
liftIO $ generateNewProject codeAgentConfig appName appDescription projectConfig liftIO $ generateNewProject codeAgentConfig appName appDescription projectConfig
where where
writeFileToStdoutWithDelimiters :: FilePath -> T.Text -> IO ()
writeFileToStdoutWithDelimiters path content = writeFileToStdoutWithDelimiters path content =
writeToStdoutWithDelimiters "WRITE FILE" [T.pack path, content] writeToStdoutWithDelimiters "WRITE FILE" [path, T.unpack content]
writeLogToStdoutWithDelimiters :: GNP.L.LogMsg -> IO ()
writeLogToStdoutWithDelimiters msg = writeLogToStdoutWithDelimiters msg =
writeToStdoutWithDelimiters "LOG" [msg] unless (null msg') $
writeToStdoutWithDelimiters "LOG" [msg']
where
msg' = U.trim $ GNP.L.toPlainString msg
writeToStdoutWithDelimiters :: String -> [String] -> IO ()
writeToStdoutWithDelimiters delimiterTitle paragraphs = do writeToStdoutWithDelimiters delimiterTitle paragraphs = do
T.IO.putStrLn . ("\n" <>) $ withDelimiter delimiterTitle $ T.intercalate "\n" paragraphs putStrLn . ("\n" <>) $ withDelimiter delimiterTitle $ intercalate "\n" paragraphs
hFlush stdout hFlush stdout
withDelimiter :: String -> String -> String
withDelimiter title content = withDelimiter title content =
T.intercalate intercalate
"\n" "\n"
[ "==== WASP AI: " <> title <> " ====", [ "==== WASP AI: " <> title <> " ====",
content, content,
"===/ WASP AI: " <> title <> " ====" "===/ WASP AI: " <> title <> " ===="
] ]
generateNewProject :: CA.CodeAgentConfig -> NewProjectAppName -> String -> NewProjectConfig -> IO () generateNewProject :: CA.CodeAgentConfig GNP.L.LogMsg -> NewProjectAppName -> String -> NewProjectConfig -> IO ()
generateNewProject codeAgentConfig (NewProjectAppName appName) appDescription projectConfig = do generateNewProject codeAgentConfig (NewProjectAppName appName) appDescription projectConfig = do
waspProjectSkeletonFiles <- readWaspProjectSkeletonFiles waspProjectSkeletonFiles <- readWaspProjectSkeletonFiles
CA.runCodeAgent codeAgentConfig $ do CA.runCodeAgent codeAgentConfig $ do
@ -123,9 +210,12 @@ generateNewProject codeAgentConfig (NewProjectAppName appName) appDescription pr
getOpenAIApiKey :: Command OpenAIApiKey getOpenAIApiKey :: Command OpenAIApiKey
getOpenAIApiKey = getOpenAIApiKey =
liftIO (lookupEnv "OPENAI_API_KEY") liftIO (lookupEnv "OPENAI_API_KEY" <&> (>>= validateKey))
>>= maybe throwMissingOpenAIApiKeyEnvVarError pure >>= maybe throwMissingOpenAIApiKeyEnvVarError pure
where where
validateKey "" = Nothing
validateKey k = Just k
throwMissingOpenAIApiKeyEnvVarError = throwMissingOpenAIApiKeyEnvVarError =
throwError $ throwError $
CommandError CommandError
@ -133,9 +223,13 @@ getOpenAIApiKey =
$ unlines $ unlines
[ "Wasp AI uses ChatGPT to generate your project, and therefore requires you to provide it with an OpenAI API key.", [ "Wasp AI uses ChatGPT to generate your project, and therefore requires you to provide it with an OpenAI API key.",
"You can obtain this key via your OpenAI account's user settings (https://platform.openai.com/account/api-keys).", "You can obtain this key via your OpenAI account's user settings (https://platform.openai.com/account/api-keys).",
"Then, add", "Then, set OPENAI_API_KEY env var to it and wasp CLI will read from it.",
"",
"To persist the OPENAI_API_KEY env var, add",
" export OPENAI_API_KEY=<yourkeyhere>", " export OPENAI_API_KEY=<yourkeyhere>",
"to .bash_profile or .profile, restart your shell, and you should be good to go." "to your .bash_profile (or .profile or .zprofile or whatever your machine is using), restart your shell, and you should be good to go.",
"",
"Alternatively, you can go to our Mage web app at https://usemage.ai and generate new Wasp app there for free, with no OpenAI API keys needed."
] ]
newProjectDetails :: NewProjectConfig -> String -> String -> NewProjectDetails newProjectDetails :: NewProjectConfig -> String -> String -> NewProjectDetails

View File

@ -1,15 +1,42 @@
module Wasp.Cli.Command.CreateNewProject.Common where module Wasp.Cli.Command.CreateNewProject.Common
( throwProjectCreationError,
throwInvalidTemplateNameUsedError,
defaultWaspVersionBounds,
printGettingStartedInstructions,
)
where
import Control.Monad.Except (throwError) import Control.Monad.Except (throwError)
import StrongPath (Abs, Dir, Path')
import qualified StrongPath as SP
import Wasp.Cli.Command (Command, CommandError (..)) import Wasp.Cli.Command (Command, CommandError (..))
import Wasp.Cli.Common (WaspProjectDir)
import qualified Wasp.SemanticVersion as SV import qualified Wasp.SemanticVersion as SV
import qualified Wasp.Util.Terminal as Term
import qualified Wasp.Version as WV import qualified Wasp.Version as WV
throwProjectCreationError :: String -> Command a throwProjectCreationError :: String -> Command a
throwProjectCreationError = throwError . CommandError "Project creation failed" throwProjectCreationError = throwError . CommandError "Project creation failed"
throwInvalidTemplateNameUsedError :: Command a throwInvalidTemplateNameUsedError :: Command a
throwInvalidTemplateNameUsedError = throwProjectCreationError "Are you sure that the template exists? 🤔 Check the list of templates here: https://github.com/wasp-lang/starters" throwInvalidTemplateNameUsedError =
throwProjectCreationError $
"Are you sure that the template exists?"
<> " 🤔 Check the list of templates here: https://github.com/wasp-lang/starters"
defaultWaspVersionBounds :: String defaultWaspVersionBounds :: String
defaultWaspVersionBounds = show (SV.backwardsCompatibleWith WV.waspVersion) defaultWaspVersionBounds = show (SV.backwardsCompatibleWith WV.waspVersion)
-- | This function assumes that the project dir is created inside the current working directory
-- when it prints the instructions.
printGettingStartedInstructions :: Path' Abs (Dir WaspProjectDir) -> IO ()
printGettingStartedInstructions absProjectDir = do
let projectFolder = init . SP.toFilePath . SP.basename $ absProjectDir
{- ORMOLU_DISABLE -}
putStrLn $ Term.applyStyles [Term.Green] $ "Created new Wasp app in ./" ++ projectFolder ++ " directory!"
putStrLn "To run it, do:"
putStrLn ""
putStrLn $ Term.applyStyles [Term.Bold] $ " cd " ++ projectFolder
putStrLn $ Term.applyStyles [Term.Bold] " wasp start"
putStrLn ""
{- ORMOLU_ENABLE -}

View File

@ -12,6 +12,7 @@ where
import Data.Either (fromRight) import Data.Either (fromRight)
import Data.Foldable (find) import Data.Foldable (find)
import Data.List (isPrefixOf)
import Data.Text (Text) import Data.Text (Text)
import StrongPath (File', Path, Rel, System, reldir, (</>)) import StrongPath (File', Path, Rel, System, reldir, (</>))
import qualified Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote.Github as Github import qualified Wasp.Cli.Command.CreateNewProject.StarterTemplates.Remote.Github as Github
@ -38,12 +39,12 @@ instance Show StarterTemplate where
show (LocalStarterTemplate metadata) = _name metadata show (LocalStarterTemplate metadata) = _name metadata
show AiGeneratedStarterTemplate = "ai-generated" show AiGeneratedStarterTemplate = "ai-generated"
instance Interactive.Option StarterTemplate where instance Interactive.IsOption StarterTemplate where
showOption = show showOption = show
showOptionDescription (RemoteStarterTemplate metadata) = Just $ _description metadata showOptionDescription (RemoteStarterTemplate metadata) = Just $ _description metadata
showOptionDescription (LocalStarterTemplate metadata) = Just $ _description metadata showOptionDescription (LocalStarterTemplate metadata) = Just $ _description metadata
showOptionDescription AiGeneratedStarterTemplate = showOptionDescription AiGeneratedStarterTemplate =
Just "[experimental] Describe an app in a couple of sentences and have ChatGPT generate initial code for you." Just "🤖 Describe an app in a couple of sentences and have Wasp AI generate initial code for you. (experimental)"
getStarterTemplates :: IO [StarterTemplate] getStarterTemplates :: IO [StarterTemplate]
getStarterTemplates = do getStarterTemplates = do

View File

@ -3,7 +3,9 @@
module Wasp.Cli.Interactive module Wasp.Cli.Interactive
( askForInput, ( askForInput,
askToChoose, askToChoose,
askToChoose',
askForRequiredInput, askForRequiredInput,
IsOption (..),
Option (..), Option (..),
) )
where where
@ -11,7 +13,7 @@ where
import Control.Applicative ((<|>)) import Control.Applicative ((<|>))
import Data.Foldable (find) import Data.Foldable (find)
import Data.Function ((&)) import Data.Function ((&))
import Data.List (intercalate) import Data.List (intercalate, isPrefixOf)
import Data.List.NonEmpty (NonEmpty ((:|))) import Data.List.NonEmpty (NonEmpty ((:|)))
import qualified Data.List.NonEmpty as NE import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T import qualified Data.Text as T
@ -37,22 +39,35 @@ import qualified Wasp.Util.Terminal as Term
We want to avoid this so users can type the name of the option when answering We want to avoid this so users can type the name of the option when answering
without having to type the quotes as well. without having to type the quotes as well.
We introduced the Option class to get different "show" behavior for Strings and other We introduced the IsOption class to get different "show" behavior for Strings and other
types. If we are using something other then String, an instance of Option needs to be defined, types. If we are using something other then String, an instance of IsOption needs to be defined,
but for Strings it just returns the String itself. but for Strings it just returns the String itself.
-} -}
class Option o where class IsOption o where
showOption :: o -> String showOption :: o -> String
showOptionDescription :: o -> Maybe String showOptionDescription :: o -> Maybe String
instance Option [Char] where instance IsOption [Char] where
showOption = id showOption = id
showOptionDescription = const Nothing showOptionDescription = const Nothing
data Option o = Option
{ oDisplayName :: !String,
oDescription :: !(Maybe String),
oValue :: !o
}
instance IsOption (Option o) where
showOption = oDisplayName
showOptionDescription = oDescription
askForRequiredInput :: String -> IO String askForRequiredInput :: String -> IO String
askForRequiredInput = repeatIfNull . askForInput askForRequiredInput = repeatIfNull . askForInput
askToChoose :: forall o. Option o => String -> NonEmpty o -> IO o askToChoose' :: String -> NonEmpty (Option o) -> IO o
askToChoose' question options = oValue <$> askToChoose question options
askToChoose :: forall o. IsOption o => String -> NonEmpty o -> IO o
askToChoose _ (singleOption :| []) = return singleOption askToChoose _ (singleOption :| []) = return singleOption
askToChoose question options = do askToChoose question options = do
putStrLn $ Term.applyStyles [Term.Bold] question putStrLn $ Term.applyStyles [Term.Bold] question
@ -83,18 +98,26 @@ askToChoose question options = do
showIndexedOptions = intercalate "\n" $ showIndexedOption <$> zip [1 ..] (NE.toList options) showIndexedOptions = intercalate "\n" $ showIndexedOption <$> zip [1 ..] (NE.toList options)
where where
showIndexedOption (idx, option) = showIndexedOption (idx, option) =
Term.applyStyles [Term.Yellow] indexPrefix concat
<> Term.applyStyles [Term.Bold] (showOption option) [ indexPrefix,
<> (if isDefaultOption option then " (default)" else "") optionName,
<> showDescription option (length indexPrefix) tags,
optionDescription
]
where where
indexPrefix = showIndex idx <> " " indexPrefix = Term.applyStyles [Term.Yellow] (showIndex idx) <> " "
optionName = Term.applyStyles [Term.Bold] (showOption option)
tags = whenDefault (Term.applyStyles [Term.Yellow] " (default)")
optionDescription = showDescription (idx, option)
whenDefault xs = if isDefaultOption option then xs else mempty
showIndex i = "[" ++ show (i :: Int) ++ "]" showIndex idx = "[" ++ show (idx :: Int) ++ "]"
showDescription option indentLength = case showOptionDescription option of showDescription (idx, option) = case showOptionDescription option of
Just description -> "\n" <> replicate indentLength ' ' <> description Just description -> "\n" <> replicate indentLength ' ' <> description
Nothing -> "" Nothing -> ""
where
indentLength = length (showIndex idx) + 1
defaultOption :: o defaultOption :: o
defaultOption = NE.head options defaultOption = NE.head options

View File

@ -1,7 +1,7 @@
app waspBuild { app waspBuild {
db: { system: PostgreSQL }, db: { system: PostgreSQL },
wasp: { wasp: {
version: "^0.11.8" version: "^0.12.0"
}, },
title: "waspBuild" title: "waspBuild"
} }

View File

@ -1,6 +1,6 @@
app waspCompile { app waspCompile {
wasp: { wasp: {
version: "^0.11.8" version: "^0.12.0"
}, },
title: "waspCompile" title: "waspCompile"
} }

View File

@ -1,7 +1,7 @@
app waspComplexTest { app waspComplexTest {
db: { system: PostgreSQL }, db: { system: PostgreSQL },
wasp: { wasp: {
version: "^0.11.8" version: "^0.12.0"
}, },
auth: { auth: {
userEntity: User, userEntity: User,

View File

@ -1,7 +1,7 @@
app waspJob { app waspJob {
db: { system: PostgreSQL }, db: { system: PostgreSQL },
wasp: { wasp: {
version: "^0.11.8" version: "^0.12.0"
}, },
title: "waspJob" title: "waspJob"
} }

View File

@ -1,6 +1,6 @@
app waspMigrate { app waspMigrate {
wasp: { wasp: {
version: "^0.11.8" version: "^0.12.0"
}, },
title: "waspMigrate" title: "waspMigrate"
} }

View File

@ -1,6 +1,6 @@
app waspNew { app waspNew {
wasp: { wasp: {
version: "^0.11.8" version: "^0.12.0"
}, },
title: "waspNew" title: "waspNew"
} }

View File

@ -1,6 +1,6 @@
app todoApp { app todoApp {
wasp: { wasp: {
version: "^0.11.0" version: "^0.12.0"
}, },
title: "ToDo App", title: "ToDo App",
// head: [], // head: [],

View File

@ -1,6 +1,6 @@
app todoApp { app todoApp {
wasp: { wasp: {
version: "^0.11.0" version: "^0.12.0"
}, },
title: "ToDo App", title: "ToDo App",
dependencies: [ dependencies: [

View File

@ -10,7 +10,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"json5": "^2.2.3", "json5": "^2.2.3",
"prisma": "^4.12.0", "prisma": "^4.16.2",
"strip-ansi": "^6.0.0", "strip-ansi": "^6.0.0",
"tmp-promise": "3.0.3" "tmp-promise": "3.0.3"
}, },

View File

@ -21,8 +21,8 @@ import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Reader (MonadReader, ReaderT (runReaderT), asks) import Control.Monad.Reader (MonadReader, ReaderT (runReaderT), asks)
import Control.Monad.State (MonadState, StateT (runStateT), gets, modify) import Control.Monad.State (MonadState, StateT (runStateT), gets, modify)
import qualified Data.HashMap.Strict as H import qualified Data.HashMap.Strict as H
import Data.String (IsString (fromString))
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T
import qualified Network.HTTP.Simple as HTTP import qualified Network.HTTP.Simple as HTTP
import System.IO (hPutStrLn, stderr) import System.IO (hPutStrLn, stderr)
import UnliftIO (Handler (Handler), catches, throwIO) import UnliftIO (Handler (Handler), catches, throwIO)
@ -35,33 +35,33 @@ import qualified Wasp.Util.IO.Retry as R
import Wasp.Util.Network.HTTP (catchRetryableHttpException) import Wasp.Util.Network.HTTP (catchRetryableHttpException)
import qualified Wasp.Util.Network.HTTP as Utils.HTTP import qualified Wasp.Util.Network.HTTP as Utils.HTTP
newtype CodeAgent a = CodeAgent {_unCodeAgent :: ReaderT CodeAgentConfig (StateT CodeAgentState IO) a} newtype CodeAgent logMsg a = CodeAgent {_unCodeAgent :: ReaderT (CodeAgentConfig logMsg) (StateT CodeAgentState IO) a}
deriving (Monad, Applicative, Functor, MonadIO, MonadReader CodeAgentConfig, MonadState CodeAgentState) deriving (Monad, Applicative, Functor, MonadIO, MonadReader (CodeAgentConfig logMsg), MonadState CodeAgentState)
data CodeAgentConfig = CodeAgentConfig data CodeAgentConfig logMsg = CodeAgentConfig
{ _openAIApiKey :: !OpenAIApiKey, { _openAIApiKey :: !OpenAIApiKey,
_writeFile :: !(FilePath -> Text -> IO ()), -- TODO: Use StrongPath? Not clear which kind of path is it, rel, abs, ... . _writeFile :: !(FilePath -> Text -> IO ()), -- TODO: Use StrongPath? Not clear which kind of path is it, rel, abs, ... .
_writeLog :: !(Text -> IO ()) _writeLog :: !(logMsg -> IO ())
} }
instance MonadRetry CodeAgent where instance MonadRetry (CodeAgent logMsg) where
rThreadDelay = liftIO . threadDelay rThreadDelay = liftIO . threadDelay
runCodeAgent :: CodeAgentConfig -> CodeAgent a -> IO a runCodeAgent :: (IsString logMsg) => CodeAgentConfig logMsg -> CodeAgent logMsg a -> IO a
runCodeAgent config codeAgent = runCodeAgent config codeAgent =
(fst <$> (_unCodeAgent codeAgent `runReaderT` config) `runStateT` initialState) (fst <$> (_unCodeAgent codeAgent `runReaderT` config) `runStateT` initialState)
`catches` [ Handler `catches` [ Handler
( \(e :: HTTP.HttpException) -> do ( \(e :: HTTP.HttpException) -> do
let errorInfo = let errorInfo =
maybe (showShortException e) show $ Utils.HTTP.getHttpExceptionStatusCode e maybe (showShortException e) show $ Utils.HTTP.getHttpExceptionStatusCode e
logMsg = T.pack $ "Code agent failed with the http error: " <> errorInfo logMsg = fromString $ "Code agent failed with the http error: " <> errorInfo
_writeLog config logMsg _writeLog config logMsg
throwIO e throwIO e
), ),
Handler Handler
( \(e :: SomeException) -> do ( \(e :: SomeException) -> do
_writeLog config $ _writeLog config $
"Code agent failed with the following error: " <> T.pack (showShortException e) fromString $ "Code agent failed with the following error: " <> showShortException e
throwIO e throwIO e
) )
] ]
@ -78,26 +78,26 @@ runCodeAgent config codeAgent =
showShortException :: forall e. Exception e => e -> String showShortException :: forall e. Exception e => e -> String
showShortException = shortenWithEllipsisTo 30 . displayException showShortException = shortenWithEllipsisTo 30 . displayException
writeToLog :: Text -> CodeAgent () writeToLog :: IsString logMsg => logMsg -> CodeAgent logMsg ()
writeToLog msg = asks _writeLog >>= \f -> liftIO $ f msg writeToLog msg = asks _writeLog >>= \f -> liftIO $ f msg
writeToFile :: FilePath -> (Maybe Text -> Text) -> CodeAgent () writeToFile :: FilePath -> (Maybe Text -> Text) -> CodeAgent logMsg ()
writeToFile path updateContentFn = do writeToFile path updateContentFn = do
content <- updateContentFn <$> getFile path content <- updateContentFn <$> getFile path
asks _writeFile >>= \f -> liftIO $ f path content asks _writeFile >>= \f -> liftIO $ f path content
modify $ \s -> s {_files = H.insert path content (_files s)} modify $ \s -> s {_files = H.insert path content (_files s)}
writeNewFile :: (FilePath, Text) -> CodeAgent () writeNewFile :: (FilePath, Text) -> CodeAgent logMsg ()
writeNewFile (path, content) = writeNewFile (path, content) =
writeToFile path (maybe content $ error $ "file " <> path <> " shouldn't already exist") writeToFile path (maybe content $ error $ "file " <> path <> " shouldn't already exist")
getFile :: FilePath -> CodeAgent (Maybe Text) getFile :: FilePath -> CodeAgent logMsg (Maybe Text)
getFile path = gets $ H.lookup path . _files getFile path = gets $ H.lookup path . _files
getAllFiles :: CodeAgent [(FilePath, Text)] getAllFiles :: CodeAgent logMsg [(FilePath, Text)]
getAllFiles = gets $ H.toList . _files getAllFiles = gets $ H.toList . _files
queryChatGPT :: ChatGPTParams -> [ChatMessage] -> CodeAgent Text queryChatGPT :: ChatGPTParams -> [ChatMessage] -> CodeAgent logMsg Text
queryChatGPT params messages = do queryChatGPT params messages = do
key <- asks _openAIApiKey key <- asks _openAIApiKey
chatResponse <- queryChatGPTWithRetry key params messages chatResponse <- queryChatGPTWithRetry key params messages
@ -105,7 +105,7 @@ queryChatGPT params messages = do
return $ ChatGPT.getChatResponseContent chatResponse return $ ChatGPT.getChatResponseContent chatResponse
where where
{- ORMOLU_DISABLE -} {- ORMOLU_DISABLE -}
queryChatGPTWithRetry :: OpenAIApiKey -> ChatGPTParams -> [ChatMessage] -> CodeAgent ChatResponse queryChatGPTWithRetry :: OpenAIApiKey -> ChatGPTParams -> [ChatMessage] -> CodeAgent logMsg ChatResponse
queryChatGPTWithRetry key params' messages' = queryChatGPTWithRetry key params' messages' =
do do
R.retry R.retry
@ -123,13 +123,13 @@ queryChatGPT params messages = do
>>= either throwIO pure >>= either throwIO pure
{- ORMOLU_ENABLE -} {- ORMOLU_ENABLE -}
getOpenAIApiKey :: CodeAgent OpenAIApiKey getOpenAIApiKey :: CodeAgent logMsg OpenAIApiKey
getOpenAIApiKey = asks _openAIApiKey getOpenAIApiKey = asks _openAIApiKey
type NumTokens = Int type NumTokens = Int
-- | Returns total tokens usage: (<num_prompt_tokens>, <num_completion_tokens>). -- | Returns total tokens usage: (<num_prompt_tokens>, <num_completion_tokens>).
getTotalTokensUsage :: CodeAgent (NumTokens, NumTokens) getTotalTokensUsage :: CodeAgent logMsg (NumTokens, NumTokens)
getTotalTokensUsage = do getTotalTokensUsage = do
usage <- gets _usage usage <- gets _usage
let numPromptTokens = sum $ ChatGPT.prompt_tokens <$> usage let numPromptTokens = sum $ ChatGPT.prompt_tokens <$> usage

Some files were not shown because too many files have changed in this diff Show More