In this chapter, we're going to implement the messages entity. This will include creating the actual message Prisma schema, the fragment Prisma schema, and then we're going to modify our current TRPC procedures and our background jobs to use those new schemas and save user prompts and AI responses in their appropriate models. So let's start by creating a simple message schema. In order to do that we have to go ahead and visit our schema file. Before you do that as always confirm that you're on your main branch and if you aren't sure if you have any unsynchronized changes you can always click this and confirm and just make sure that chapter 7 is your last merge change here.
Great, now let's go ahead inside of Prisma and schema.prisma. If you have a folder with migrations here, you can delete it because we're going to remove pretty much everything inside of here, right? So we're going to create a whole new schema now. So let's go ahead and create a model message. Inside create an ID which will be a type of string.
It's going to be an ID with the default value of UUID. After that let's go ahead and create a content which will be a type of string. Let's go ahead and add a role which will be a type of enum. So let's create an enum message type. And let's give it, my apologies, not message type, message role, which can be a type of user or assistant.
And then you can go ahead and use that right here. So simply assign the role to the message role, just like that. And now we're going to do what I started to do, which is the message type. So the message type will either be a type of result or a type of error. And let's go ahead and give this a type of message type.
So in case the AI response fails, we're going to treat it as an error, meaning that the AI will simply return, I wasn't able to do this generation for whatever reason, please retry. And now let's add the usual created at field, which is a type of date time and the default value of now. And let's add updated at, which is a date time as well, and it has a special decorator updated add which is a very cool decorator because what it does is it will automatically update this field when we update the message model. And now let's go ahead and let's create a fragment model. So the fragment model will also have an ID of string and the default value of UUID and it will have a relation to the message.
So let's add a message ID to be a type of string and it needs to be unique. Now let's add a message here to be a type of message, give it a relation decorator, targeting the field's message ID which we defined above, referencing the ID field in the message model, and let's add on delete here to be cascade. So if this message gets deleted, the fragment gets deleted as well. And now we just have to fix this error by adding a proper relation here in the message. So the message does not have to have a fragment.
If the user is sending a message, there will be no fragment. Only for the AI response will there be a fragment. That's why we're going to create a fragment field. And we're going to make it a type of fragment, but it's going to be optional, like this. And then let's go back inside of the fragment model, and let's create a sandbox URL to be a type of string, the title to be a type of string, and files to be a type of JSON.
So this is quite cool. It's very nice that Postgres allows this and it's perfect for our use case because files is not exactly something that in my opinion makes sense to create a whole new model for because it's just a simple mapping of the file path and the content and it can be pretty much infinite in size. Well, obviously not infinite, but you know what I mean. So I think this is a very good use case of using JSON in Postgres. And then we can just copy the created app and the updated app from the model above.
Just like that. Now in here, we should have no errors. And again, make sure that you're using the Prisma extension simply so you have the syntax highlighting and it will tell you in advance if you've done anything incorrectly here. So what we have to do now is we have to push this. So let's go ahead and shut down our app.
Make sure you have shut down your Ingest server as well. And I will now run npx prisma migrate dev. And let's just wait a second for this to connect to our database. So I have gotten an error. In your case, you might not get an error, but I think this is because, yes, it detected some drift.
Your database schema is not in sync with your migration history. That's because I told you to manually delete the migration folder. This is not a problem. We're working with development data here. So let's use npx Prisma migrate reset first.
And let's just reset the entire thing. So let me just confirm this. And even if this doesn't work, you can always just create a new Postgres database and then just go instead of .environment and just use a new database URL. That's like the ultimate brute force you can do. So after I've done my migrate reset, I will try migrate dev again.
And this time with no problems, I'm going to call this message dash fragment. And there we go, just like that, we have created a new schema here. And now we can go ahead and run the NPX Prisma Studio. And in here you should see the fragment and the message as the models, meaning it successfully created that. Perfect.
So now let's go ahead and let's actually use these things. So what I want to do now is I want to go inside of source and I want to create a new folder called modules. So I like to have a module based structure in my application. So instead of having my procedures written here randomly, I will have them in their own module. So I like to separate modules either by large chunks of my application, like home page, landing page, pricing, or by entity models that I have in my database.
So for example, let's go ahead and let's create messages in here. So inside of here, I will keep everything message related. So for example, one of those things would be all the things that go on the server regarding messages, specifically all our procedures. So now that we are inside of here, we're going to import init trpcrouter or create trpcrouter from trpcinit and let's export const messages router here to be create trpc router. And then inside of here, let me just quickly check instead of my trpc routers app, I seem to have this existing one called invoke.
So now what we're going to do is we're going to create a create procedure. So this will be accessed through as message dot create. This is how you will call this. That's why it's called create and not create message because it would be redundant message dot message create or create message right Makes no sense. So let's add a base procedure, which will of course be a protected procedure later on in the tutorial.
For now, it's perfectly fine to be a base procedure. Let's go ahead and define an input here. And I'm going to set this to be the value. It can be the value, it can be the prompt. I think value is good enough.
And let me just import Z from Zod. Let's go ahead and set it to be a string. And let's give it a message is required error. Great. And then let's go ahead and let's chain mutation here.
It's going to be asynchronous. Let's destructure the input from here like this. And then inside of here, what we are going to do is we're going to create a new message by using await Prisma from lib database, and then go ahead .message.create and pass in the data inside. And let's add the content to be input.value here like this. And let me just see What else do I have to add inside?
Because I already forgot how my schema looks like. So I have to add a role and I have to add a type. So my role here will be user, and my type here will be result. All right, there's no loading. This is an Instant message created by the user.
Perfect. So I think I actually don't even need to put that in any type of constant. I think this works just fine. And what I'm doing after this is I'm actually invoking my background job. So let me go inside of the routers here, and let me just copy this part.
Inside of procedures, and let's go right here. So let me import ingest from the ingest client. Let me show you my imports a bit simply So you're on the same page. There we go. And obviously, we're going to have to change this as well.
It makes no sense to be called test. And this is what we should actually do. We should keep this as created message or a new message. And then simply return created message. Simply so our API response has some kind of, well, response for the user back.
Perfect. Now that we have the basic message router created with some basic validation here. Let's go ahead inside of trpc-routers and let's remove everything inside of here. And then in here add messages, messages-router. You can import the ingest, you can remove zone, you can remove ingest, and you can remove the base procedure.
Just like this. And this is how we're going to add all other module related things inside. So later when we add fragments, it will be fragments-router. And we will control all procedures inside of its own module, right? Like this.
Great. So now, obviously, we need to fix some things in our page, I believe. So let's go inside of source app page. And in here, this is now create message. This will be trpc.messages.create.
The onSuccess is the same. In here, we can just say onSuccess message created. And then let's go ahead and let's use createMessageIsBending and createMessage.mutate, just like that. So right now, this should still work exactly the same, right? Let's go ahead and just quickly try it out, just to make sure we didn't accidentally break something.
So npm run dev in one, npx ingest cli latest dev in the other one. Let's go and we can install the new one if it appears it's okay and let's go ahead and open localhost 3000 here and I'm going to do create a landing page simply because this is the simplest thing that most likely won't go wrong. There we go, looks like it is created So I'm going to click Invoke a Background Job here. And it looks like message was created. I'm going to go inside of my Ingest Developer Server here.
And I'm going to wait for this to complete. And here we have it. It is complete and quite a nice result. I'm always impressed by its landing pages. It seems to have gotten very good at creating landing pages.
Great, so looks like everything is still working. And now what we have to do is while we are storing the messages from the user, we are not storing the messages from the AI. So in order to keep track of that, how about we extract the messages here by using use query from tanstack query so just make sure you add this import here pass in trpc.messages and I just remembered we didn't create any So let's simply go inside of the messages router, which is inside of your modules here, and simply create, let's call this getMany, base procedure. The input doesn't really matter for now, let's just do a query here. Again, it's going to be an asynchronous method.
And in here, what we're going to do is get the messages to be await Prisma message, find many. And how about we do order by and let me just see I have to use updated at or created at let's use ascending and return the messages like this and just like that we have our get many procedure So now we can go back here and add it. There we go. GetMany. This will be query options here.
And then inside of here, let's go ahead and do it below the button. JSON.stringify messages null 2. So now you can see that I just created this create a landing page with the role user. So if I go ahead and do create a red landing page, and invoke this background job and refresh this page, you can see that now I have create a landing page and after that I have a create a red landing page. So let me go inside of my procedures and change the updated ad to be descending and refresh and then the newer message appears at the top.
And if you want you can wait for the result but you know it's it's not that important right now. But it definitely created a red landing page. Great. So now, again, I'm expecting that besides having these steps to get sandbox ID, create our update, and then finalize, I also needed to save this entire thing to the database so that we can access it from the UI and not from the Ingest developer server. So let's go ahead back inside of ingest functions here and then we're going to create a whole new step here at the bottom.
So after we get our sandbox URL we have to go ahead and actually save this to the database. So let's do await step.run saveResult. Asynchronous method like this. And in here, let's return await Prisma.message. Oops, we have to import Prisma from lib database, so just make sure you add this import.
And we're basically going to save the content. So prisma.message.create. The data will be the following. Content is going to be result, state, data, summary. And the role will be assistant and type here will be result like this.
And let's also extend it a bit further by also creating the fragment relation. Let's pass in the sandbox URL here. Sandbox URL doesn't exist. Did I do something incorrectly in my schema, Or is it just the syntax that I didn't finish? It definitely does exist here.
So perhaps I just have to do result, actually sandbox URL, like this. Oh, my apologies. This is not how you do it. Create, and then sandbox URL, sandbox URL. And let's go ahead and add the title of the fragment to be just fragment and files can be result state data files there we go So now let's go ahead and try and do this again.
So use a simple prompt again, build a blue landing page. So basically something simple and then wait for this to finish. And after it finishes, you should now see another step happening here, which is to save the result in Prisma. It should create a assistant message, and it should also create a fragment with the sandbox URL and all the files that it created. There we go, so we have the save result step and now if I go back here, there we go.
You can see I have a new message here at the top. The content includes the task summary, created a fully responsive production quality blue themed landing page in app page TSX. The layout includes a navbar, hero section, favorites, pricing, contact form and footer. And we don't really have access to the sandbox URL here that's because what we have to do if we want to see that is go inside of the message router and we have to add include fragment true and after you do that you will see the entire fragment content So you will see the entire source code actually and you will see the sandbox URL. So if you try adding that here we now have the blue landing page.
And that is basically what we now have to do. So let's remove the fragment for now. We can easily add it later because I'm sure if we need it and it's taking up a lot of space. Perfect! So this is what I actually wanted to do for this chapter.
I wanted us to add the messages router And I think that we can do one more thing while we are here. And that is the following. We can go inside of ingest here instead of functions, and let's just change this, right? Let's stop calling it. Hello world.
Let's go ahead and call this code agent the id will be code agent and let's also change the event to be code agent run like this And now just make sure that you go back and set up your modules, messages, server, procedures here. And when you invoke it, make sure to change this like that. Or any other place where you do this, make sure to change it. For me, it's only one place. CodeAgent run.
OK, and now we have to also go inside of app API ingest route, and we have to replace this with CodeAgent. There we go. Go ahead and refresh. If it's still stuck, you can always cut this down. And I would recommend shutting both of them down.
And let's go ahead and refresh again. Build a green landing page. Message created. And I recommend waiting it out just to confirm it works, since we just changed this to be a code agent function. There we go.
Seems to work quite well. And I think that is it for this chapter. Oh, this one's nice. We've basically created the message model, the fragment model, which basically puts us in a position where we can start creating proper UI around this. Because by having the fragment and by having the message, we can create the file explorer, we can create the iframe where we render the URL, and we can create the message containers on this side.
And while we are here, it is important to do one more thing. Go inside of your functions.ts in the ingest here. And after you do the result from the network run, define an isError constant. And it will be an error if we don't have resultData.state, my apologies, stateData.summary, or if object.keys resultStateData my apologies, state data summary or if object.keys result state data files or an alternative empty array dot length is equal to zero. So if any of those two are missing, it means something went wrong.
So inside of here, when we save the result, what we're going to do is we're going to check if is error, we're going to return content. Whoops, my apologies. We are going to return await, Prisma message create data content. Something went wrong. Please try again.
Like this. Let's give it a role of assistant and let's give it a type of error. Like this. There we go. So we do an early return if we detect it is an error So we don't create the fragment if we don't have the information to create it And the one thing I completely forgot about is the types here So right now files is a type of any, summary is a type of any, right?
And while this seems to not create any problems for us, I want to show you that there is a way so that you can properly type your entire network state, because I think that is important, and it will make your project more maintainable. So let's go ahead above the code agent here and let's create an interface agent state. And let's go ahead and do the following. Let's make a summary a string and let's create files which can be mapped as a record string string but I don't like this simply because there is a way to make it closer to what we expect and it's basically opening an object and then defining path as the key and simply the content as a string. I think this more closely resembles what we expect, rather than the record string string.
Now that we have the agent state, we just have to find all the places to use it. Starting with, oh yes, I really don't like this. We should not name our function and our agent the same. So how about we rename one of them? Let's call this code agent function like this.
And then go back inside of your source app API in just route code agent function code agent function like this way safer like that okay now let's go ahead and use the agent state instead of the code agent here. We can open pointy brackets and pass it inside. So that's step one. Then the next place we can use it is in the tool create or update files. So in here we have step and network and you can see that files here are undefined even though we added it to the agent state.
That's because what we have to do here is we have to define this step as a type of tool from Ingest Agent Kit. So just make sure that you import the type tool from Ingest AgentKit. I think you can specify type like this. Let's go back here. So it's going to be a type of tool, .options, and pass in agent state inside.
And then when you hover over files, you will see that it has the correct state. So that's the second place. And the third place is in the network here. So open this up agent state like this, and then data dot summary is a type of string now and you will see that you now have auto-complete. And if you type something else, you should get an error now, right?
So when you clearly define your state, it is much stricter and you will not be making any mistakes now. Perfect. So I think that this is it for this chapter, then. And let me just check, how does this look like? Summary, so this looks like it doesn't need anything, because the lifecycle seems to infer properly from create agent agent state here right so if I change this I'm getting an error perfect great so I think that this could be it for this chapter so I'm going to stop here let me just fix this fix this description coding agent like this and of course yeah if you want you can change the name of this.
I told you, you can always go inside of your, let me find a folder, sandbox templates, toml file, And you can change the name here. And then simply run inside of this folder e2b-template-build. Great. So now that we have this, let's go ahead and open a pull request. If you want to you can also you know try another one just to confirm it works because we changed again the name of our function but at this point I don't think you know how to fix it but let's just try build a yellow landing page Just for sanity checks I don't end the chapter and things are broken.
And seems to work just fine. Let's go ahead and see the yellow landing page. Perfect! So let's go ahead and open a pull request. So this chapter is 08 messages.
We just created the message schema, fragment schema, we're saving the user prompt and we are saving the user response. Perfect. So I'm going to go ahead and I'm going to create a new branch, 08 messages. I'm going to stage all of my changes. I'm going to add a commit message.
And I'm going to commit and publish the branch. If you want to, there is a free CodeRabbit extension, which can help you review all of your files here. Now let's go ahead and go inside of our github and let's go ahead and open a pull request and let's review with the summary and the diagram here. And here we have a summary so let's quickly go over it so we end this chapter. We introduced a new messages system allowing users to create and view messages with associated metadata and fragments.
Messages now display additional details including message type and role. And we did some refactors such as we streamlined the backend procedures and routing for the message management, and we removed the legacy user and post data structures. Perfect. So in here we have the diagram, but nothing much has been changed from last time, except this time we have additional step before we invoke the code agent run, which is that we save the user message in the database and we have one more step in the background job where we save the message to the database to the Prisma here. Great and in here we have some comments but all of these things will be changed.
The onError will be added here later. This will basically not be in this component at all so that's the only reason why I keep you know not fixing these comments. Not because they're wrong, they're completely right but it's not the component they are going to be in anyway this is just for demonstration right we are now going to start and build the proper UI. In here I'm pretty sure this is not needed simply because Ingest events have their own try-catch methods. So let's go ahead now and go through the rest of these.
So in here it recommends pagination, that's something we can look into later, but yes it's very easy to add pagination with Prisma. As you can see they have take, they have skip and that's pretty much all you need here. In here it recommends limiting the length of the message and that is definitely a good thing. Yeah, we don't want any user to be able to spam our app with a huge number of tokens so we will have to limit this to some reasonable number. This is a very good suggestion here.
Let's go ahead and merge our pull request. As always, I'm not going to delete my branch. Instead, what I'm going to do is I'm going to go back to my main branch here and I'm going to click on Synchronize Changes. And once that is finished, I can go inside of my source graph and confirm messages are the last merged chapter. Amazing, amazing job and see you in the next chapter.