Posted on Jun 28 • Originally published at Wasp-lang.dev LangChain, ChatGPT, and other emerging technology have made it possible to build some really creative tools. In this tutorial, we’ll build a full-stack web app (LangChainJS, React, NodeJS) that acts as our own personal Twitter Agent, or “intern”, as I like to call it. It keeps track of your notes and ideas, and uses them — along with tweets from trending-setting twitter users — to brainstorm new ideas and write tweet drafts for you! 💥The app we will be building here is a simplified version of the Banger Tweet Bot I built for my own twitter marketing needs. You can take the app for a spin, and you can also check out the full, final repo here: https://github.com/vincanger/banger-tweet-botWasp = } is the only open-source, completely serverful fullstack React/Node framework with a built in compiler that lets you build your app in a day and deploy with a single CLI command.We’re working hard to help you build performant web apps as easily as possible — including making these tutorials, which are released weekly!We would be super grateful if you could help us out by starring our repo on GitHub: https://www.github.com/wasp-lang/wasp 🙏…even Ron would star Wasp on GitHub 🤩⭐️ Thanks For Your Support 🙏Twitter is a great marketing tool. It’s also a great way to explore ideas and refine your own. But it can be time-consuming and difficult to maintain a tweeting habit.That’s why I decided to build my own personal twitter agent with LangChain on the basis of these assumptions:🧠 LLMs (like ChatGPT) aren’t the best writers, but they ARE great at brainstorming new ideas.📊 Certain twitter users drive the majority of discourse within certain niches, i.e. trend-setters influence what’s being discussed at the moment.💡 the Agent needs context in order to generate ideas relevant to YOU and your opinions, so it should have access to your notes, ideas, tweets, etc.So instead of trying to build a fully autonomous agent that does the tweeting for you, I thought it would be better to build an agent that does the BRAINSTORMING for you, based on your favorite trend-setting twitter users as well as your own ideas.Imagine it like an intern that does the grunt work, while you do the curating!In order to accomplish this, we need to take advantage of a few hot AI tools:Embeddings and Vector Databases give us a powerful way to perform similarity searches on our own notes and ideas. If you’re not familiar with similarity search, the simplest way to describe what similarity search is by comparing it to a normal google search. In a normal search, the phrase “a mouse eats cheese” will return results with a combination of those words only. But a vector-based similarity search, on the other hand, would return those words, as well as results with related words such as “dog”, “cat”, “bone”, and “fish”.You can see why that’s so powerful, because if we have non-exact but related notes, our similarity search will still return them!For example, if our favorite trend-setting twitter user makes a post about the benefits of typescript, but we only have a note on “our favorite React hooks”, our similarity search would still likely return such a result. And that’s huge!Once we get those notes, we can pass them to the ChatGPT completion API along with a prompt to generate more ideas. The result from this prompt will then be sent to another prompt with instructions to generate a draft tweet. We save these sweet results to our Postgres relational database.This “chain” of prompting is essentially where the LangChain package gets its name 🙂This approach will give us a wealth of new ideas and tweet drafts related to our favorite trend-setting twitter users’ tweets. We can look through these, edit and save our favorite ideas to our “notes” vector store, or maybe send off some tweets.I’ve personally been using this app for a while now, and not only has it generated some great ideas, but it also helps to inspire new ones (even if some of the ideas it generates are “meh”), which is why I included an “Add Note” feature front and center to the nav barOk. Enough background. Let’s start building your own personal twitter intern! 🤖BTW, if you get stuck at all while following the tutorial, you can always reference this tutorial’s repo, which has the finished app: Twitter Intern GitHub RepoWe’re going to make this a full-stack React/NodeJS web app so we need to get that set up first. But don’t worry, it won’t take long AT ALL, because we will be using Wasp as the framework. Wasp does all the heavy lifting for us. You’ll see what I mean in a second.Great! When running wasp start, Wasp will install all the necessary npm packages, start our server on port 3001, and our React client on port 3000. Head to localhost:3000 in your browser to check it out.Tip 💡you can install the Wasp vscode extension for the best developer experience.You’ll notice Wasp sets up your full-stack app with a file structure like so:Let’s start adding some server-side code.Start by adding a .env.server file in the root directory of your project:We need a way for us to store all our great ideas. So let’s first head to Pinecone.io and set up a free trial account. In the Pinecone dashboard, go to API keys and create a new one. Copy and paste your Environment and API Key into .env.serverDo the same for OpenAI, by creating an account and key at https://platform.openai.com/account/api-keysNow let’s replace the contents of the main.wasp config file, which is like the “skeleton” of your app, with the code below. This will configure most of the fullstack app for you 🤯Note 📝You might have noticed this {=psl psl=} syntax in the entities above. This denotes that anything in between these psl brackets is actually a different language, in this case, Prisma Schema Language. Wasp uses Prisma under the hood, so if you've used Prisma before, it should be straightforward.As you can see, our main.wasp config file has our:With this, our app structure is mostly defined and Wasp will take care of a ton of configuration for us.But we still need to get a postgres database running. Usually this can be pretty annoying, but with Wasp, just have Docker Deskop installed and running, then open up another separate terminal tab/window and then run:This will start and connect your app to a Postgres database for you. No need to do anything else! 🤯 Just leave this terminal tab, along with docker desktop, open and running in the background.In a different terminal tab, run:and make sure to give your database migration a name.If you stopped the wasp dev server to run this command, go ahead and start it again with wasp start.At this point, our app will be navigating us to localhost:3000/login but because we haven’t implemented a login screen/flow yet, we will be seeing a blank screen. Don’t worry, we’ll get to that.First though, in the main.wasp config file, let’s define a server action for saving notes and ideas. Go ahead and add the code below to the bottom of the file:With the action declared, let’s create it. Make a new file, .src/server/ideas.ts and add the following code:Info ℹ️We’ve defined the action function in our main.wasp file as coming from ‘@server/ideas.js’ but we’re creating an ideas.ts file. What's up with that?!Well, Wasp internally uses esnext module resolution, which always requires specifying the extension as .js (i.e., the extension used in the emitted JS file). This applies to all @server imports (and files on the server in general). It does not apply to client files.Great! Now we have a server action for adding notes and ideas to our vector database. And we didn’t even have to configure a server ourselves (thanks, Wasp 🙂).Let's take a step back and walk through the code we just wrote though:Now we want to create the client-side functionality for adding ideas, but you’ll remember we defined an auth object in our wasp config file. So we’ll need to add the ability to log in before we do anything on the frontend. Let’s add that quickly by adding a new a Route and Page definition to our main.wasp file…and create the file src/client/LoginPage.tsx with the following content:Info ℹ️In the auth object on the main.wasp file, we used the usernameAndPassword method which is the simplest form of auth Wasp offers. If you’re interested, Wasp does provide abstractions for Google, Github, and Email Verified Authentication, but we will stick with the simplest auth for this tutorial.With authentication all set up, if we try to go to localhost:3000 we will be automatically directed to the login/register form.You’ll see that Wasp creates Login and Signup forms for us because of the auth object we defined in the main.wasp file. Sweet! 🎉But even though we’ve added some style classes, we haven’t set up any css styling so it will probably be pretty ugly right about now. 🤢 Barf.Luckily, Wasp comes with tailwind css support, so all we have to do to get that working is add the following files in the root directory of the project:postcss.config.cjstailwind.config.cjsFinally, replace the contents of your src/client/Main.css file with these lines:Now we’ve got the magic of tailwind css on our sides! 🎨 We’ll get to styling later though. Patience, young grasshopper.From here, let’s create the complimentary client-side components for adding notes to the vector store. Create a new .src/client/AddNote.tsx file with the following contents:Here we’re using the embedIdea action we defined earlier to add our Idea to the vector store. We’re also using the useState hook to keep track of the idea we’re adding, as well as the loading state of the button.So now we have a way to add our own ideas and notes to our vector store. Pretty sweet!To pull the tweets from our favorite trend-setting twitter users, we’ll be using a package, Rettiwt, that pulls the information from the unauthenticated public twitter page. This is perfect for our purposes since we only want to fetch tweets from our selected twitter "trend-setters" and don't need to interact with the official twitter API.To achieve this, we will perform the following steps:So let’s start again by creating our LangChain function. Make a new src/server/chain.ts file:Great! Let's run through the above code real quick:Vector Cosine Similarity Scores 🧐 A good similarity threshold for cosine similarity search on text strings depends on the specific application and the desired level of strictness in matching. Cosine similarity scores range between 0 and 1, with 0 meaning no similarity and 1 meaning completely identical text strings.In our case, we went for a moderate similarity threshold of 0.7, which means that we will only return notes that are at least 70% similar to the example tweet.With this function, we will get our newTweetIdeas and our interestingTweet draft back as results that we can use within our server-side action.Now let’s define that action in our main.wasp file…and then create that action within src/server/ideas.tsOk! Nice work. There’s a lot going on above, so let’s just recap:Phew! We’re doing it 💪 Since we now have our chain of GPT prompts defined via LangChain and our server-side action, let’s go ahead and start implementing some front-end logic to fetch that data and display it to our users… which is basically only us at this point 🫂.Just as we added a server-side action to generateNewIdeas we will now define a query to fetch those ideas.Add the following query to your main.wasp file:In your src/server/ideas.ts file, below your generateNewIdeas action, add the query we just defined in our wasp file:With this function we will be returning the tweet drafts we generate, along with our notes, the original tweet that inspired it, and the newly generated ideas. Sweet!Ok, but what good is a function that fetches the data if we’ve got nowhere to display it!? Let’s go now to our src/client/MainPage.tsx file (make sure it’s got the .tsx extension and not .jsx) and replace the contents with these below:At this point, you. might need to restart the wasp dev server running in your terminal to get the tailwind configuration to take effect (ctrl + c, then wasp start again).You’ll now be prompted with the login / register screen. Go ahead and click on register and you will be automatically logged in and redirected to the main page, which at this point only has this:Let’s go back to our MainPage.tsx file and add the magic!First, let’s create a buttons component so we don’t have to constantly style a new button. Create a new src/client/Button.tsx file:Now let’s add it to your AddNote.tsx component, replacing the original button with this one. The whole file should look like this:Noice. Next, we want our page to perform the following actions:That’s exactly what the below code will do. Go ahead and replace the MainPage with it and take a minute to review what’s going on:This is what you should see on the homepage now! 🎉But, if you clicked ‘generate new ideas’ and nothing happened, well that’s because we haven’t defined any favorite trend-setting twitter users to scrape tweets from. And there’s no way to do that from the UI at the moment, so let’s open up the database manager and add some manually.In a new terminal tab, in the root of your project, run:Then, in a new browswer tab, at localhost:5555 you should see your database. Go to user, and you should be the only user in there. Add the usernames of a couple of your favorite trend-setting twitter users. Make sure the accounts have tweeted recently or your function won’t be able to scrape or generate anything! Hey ✋While you’re at it, if you’re liking this tutorial, give me a follow @hot_town for more future content like thisAfter adding the twitter usernames, make sure you click save 1 change. Go back to your client and click the Generate New Ideas button again. This might take a while depending on how many tweets it’s generating ideas for, so be patient — and watch the console output in your terminal if you’re curious ;)Awesome! Now we should be getting back some generated ideas from our twitter “intern” which will help us brainstorm further notes and generate our own BANGER TWEETS.But it would be cool to also display the tweet these ideas are referencing from the beginning. That way we’d have a bit more context on where the ideas came from.Let’s do that then! In your MainPage file, at the very top, add the following import:This allows us to embed tweets with that nice twitter styling.We already added this dependency to our main.wasp file at the beginning of the tutorial, so we can just import and start embedding tweets.Let’s try it out now in our MainPage by adding the following snippet above our