In this chapter, we're going to learn how to generate embeddings using convex RAG or retrieval augmented generation component. Just before we jump into that, I want to fix one little oversight from the previous chapter. And that is, if a conversation is unresolved and the operator decides to send a message in the middle of the conversation between the AI and the customer we should forcefully change that status to be escalated to automatically turn off AI. So let's quickly do that. Inside of packages backend convex let's go inside of private messages and when the message gets created which will be by the operator.
In here I think we already check if the status is resolved and we throw the error. So now let's check if the conversation status is unresolved and if it is unresolved we're going to force the conversation ID to change the status to escalated. And let me just use the arguments.conversationID here. So why am I doing this? Well, if you take a look at the messages in the public convex folder and find create here, you will see here that we check if the conversation status is unresolved.
And only if it's unresolved, do we actually trigger AI response. Otherwise, we just save the user's message. So that's why we're doing that. So if the user intercepts here, we are switching the status to escalated, meaning, all right, the operators basically wanted to interrupt the conversation between the AI and the user. It means the operator wants to talk to the user directly.
This means this conversation has escalated. The AI is no longer handling this case. So that's why I wanted to add this only if the current status is unresolved. Great. And now what I want us to do is I want us to learn how to create embeddings using convex.
So let's start by creating the files functions. So we're gonna go inside of packages, backend, convex, private, and let's create files.ts. And inside of here, let's start by creating a very simple add file, which will be an action. Now the arguments here are going to be file name which is a string I never knew how to pronounce this mime type I guess which is a string bytes which will be a type of bytes and category which will be optional and a string category will help with embedding and making it more specific. Now let's add our handler here.
Let's import the convex values so we get rid of those errors. Let's extract the context and the arguments from here. And even though this is an action, we can still actually verify our identity. So let's quickly steal that from messages, for example. We check the identity and we check for the organization ID.
So let's copy this and let's add it here so like this and import the convex error from convex values so we can still get the identity and they can still get the organization id great now that we have this let's go ahead and let's destructure bytes, filename and category from arguments. Now let's go ahead and let's determine the MIME type. So that's going to be arguments.mimeType or let's use guessMimeType which should be a method we're going to create now. This method will accept the file name and the bytes. Let's go ahead and let's create the guessMimeType method.
So here at the top I'm going to create a function guessMimeType. The file name will be a type of string and the bytes will be a type of array buffer and it will return back a string. Now inside of here we're going to have to import our convex dev rag component but if you try adding that now it's not going to work because we didn't add it. Let's try. So if I add guessMimeType from contents from at convex-dev, you can see we only have agent.
We don't have rag, which is the component we need. So let's learn how to add it. Instead of your agents here, you have rag. Let's go ahead and let's add it here. So install the rag component.
Let's go ahead and click here so we'll learn how to do it. Let's go ahead and run convex dev rag. So I'm going to do pnpm f back end add convex dash dev forward slash rag. And I'm also going to show you the exact version that got installed here. You can see how now we no longer have this error.
And let me just show you. So 0.3.3 is my version. But we're not done yet. So after we install it, We have to go inside of convex.config.ts. So let's go ahead inside of convex.config.ts here.
Let's go ahead and let's import rag, the same way we imported the agent. And then let's add app.use rag. Just like that. And I believe that's it for now. Great.
So now we can follow this thread going forward but let's go back inside of files now and now we have this guess mime type from contents but that's not going to be the only thing we're going to import here. We're also going to import guessMimeTypeFromExtension. So now let's go ahead and return, open parenthesis, guessMimeTypeFromExtension and pass in the file name. If that's not available, let's do guess MIME type from contents and pass in the bytes. And if that's not available, we're just going to label this as application and again not sure how to pronounce this, octet stream.
Let's go ahead and end this function. So that is our guessMimeType function. And now down here we have the MIME type which will be a string. So either the one we pass from the arguments or the one from here. And I think we could also extract the MIME type here and then we don't...
Oh, yeah, let's not do that because this is called MIME type. Now that we have the MIME type let's define the blob here. New blob, open an array and add the bytes inside and give it a type MIME type. Now let's go ahead and let's store the file. So cons, storage ID, await, context, storage, store and pass in the blob.
It is actually that easy to store an uploaded file from Convex. So yes, Convex has built-in file storage if you didn't know. You can go and click upload here and you can see different ways of uploading. One way you can do it is by using generate upload URL. You can do it using actions like we're doing.
You can also do it using HTTP actions, basically a bunch of options that you can do. Let's go ahead and continue developing here. Now what we have to do is we have to extract the text content from whatever file we just uploaded. And The way we're going to do that is by defining constant text await and then we're going to create a new method extract text content and It's going to accept the context and then in its second parameter. It will accept the storage ID file name bytes and MIME type Now let's go ahead and let's develop this component.
So this one will be quite large, so I don't want to write it in this file. Instead, I'm going to go inside of convex folder and I will create a new folder called lib. And inside I'm going to create extract text content.ds. Again, remember, inside of convex, we have to use camel case. Is this called camel case?
I think it is. Yeah. And let's go ahead now and let's import OpenAI from AI SDK OpenAI or whatever it is that you use. So let me just find this. So in my case, you've already done this inside of messages, private, inside of support agent, whatever you use here, Gemini or whatever else you used.
Now let's also import generate text. And let's import, I mean, generate text from AI. That's it. Let's go ahead and import type. Storage action writer from convex server.
Let's import assert from convex helpers. Convex helpers is actually a super useful little library. I highly advise that you search for it. So this assert will basically help us validate truthness at runtime, providing a type guard. You're going to see what it is in a second.
So yes, if you have a constant that is string or null, you can just run assert x and from that line and below x, it will be definitely a string. So it's super useful when writing a ellipse like this. Let's create a factory of AI models that we're going to use to create embeddings and to extract text from uploaded files. So if we receive an image, I'm going to use openai.chat and I'm going to pass in gpt4o-mini. For you, this would be, I mean, if you're using something else, I think this is the same thing I've been using everywhere.
So yeah, whatever you used in your support agent or your messages, just use it again here for the image. For PDF, openai.chat. I think you can use the same thing. I'm going to use 4.0 because it's better at extracting text from PDFs. But I'm pretty sure it would work exactly the same if it was 4.0 mini.
So if you're not sure what to use for your AI model, you can just use the same thing. If we receive HTML file, chat GPT 4.0. And let's set as constant. Now let's go ahead and let's write the supported image types. So why are we supporting this?
So we basically want to allow the user to upload whatever they want. If they have an image of some text, we want to allow them to do that. If they have a PDF, sure. HTML, whatever. You can even do audio if you want to.
The only reason I'm not doing audio here is because I'm not sure if the model you're using has transcribe function. Not all models have them. So I'm only doing what all models have. So the supported image types are jpg, png, webp and gif. And let's also add as const here.
And now let's go ahead and define const system prompts. So for image We're going to say you turn images into text. If it is a photo of a document, transcribe it. If it is not a document, describe it. PDF, you transform PDF files into text.
HTML, you transform content into markdown. So we are basically telling the AI model what to do if it receives an image, what to do if it receives PDF and what to do if it receives an image, what to do if it receives PDF, and what to do if it receives HTML. Now let's export type, extract text content arguments. We're gonna have storage ID, which is a type of ID from generated data model. And it's going to be a type of underscore storage.
File name will be a type of string. Bytes will be an optional array buffer, and mime type will be a type of string. And now let's export asynchronous function extract text content. The first argument it's going to accept is context. And that's going to be an object which has storage in it, storage action writer.
This is the type that we imported from convex server. The second argument will be arguments. And those will be extract text content arguments. So how did I know these are the ones that are going to be inside? Well remember, I'm the one calling this function and I'm passing storage ID, file name, bytes and MIME type.
And I'm passing context. But the only thing I will need the context for is for the storage. So that's what I'm giving it the types for. It doesn't need to have the other types. So in here I'm just translating what I wrote here.
Now that we have that let's go ahead and define the return type. It's going to be a simple promise which returns a string. Now let's go ahead and let's destructure all of those things from our arguments here. Storage ID, file name, bytes and MIME type. Let's go ahead and let's get the URL.
We can get the URL of the file by calling await context storage because remember just moments ago we stored that file inside of our file storage and now convex gives us a super easy way to turn that storage id into actual url that we can access so get url storage id so yes just by having the storage ID file, you can't do much. You need to use their get URL to actually turn it into the URL. So super useful file upload thingy. And here's the thing, yeah, so URL can be null. And now that will be a little bit problematic to work with.
So thanks to assert from convex helpers, we can just run assert URL And then in the second argument the error to throw. Failed to get storage URL. And if you try and type URL from now, so let's just do const a URL, you will see that URL is always a type of string. But before this it was string or null. I think these kinds of things are super useful and convex helpers are full of things like this so make sure to Google convex helpers and you will find much more about that than what I did in this tutorial.
Let's check if we uploaded a supported image type or not. So if supported image types some type, type is equal MIME type. Let's go ahead and return extract image text and pass in the URL. And now let's go ahead and develop extract image text to extract the text from an image. So asynchronous function, extract image text will accept the URL, which is a type of string.
And just like that, the errors are gone. The return will be a promise string. Let's go ahead and let's do const result await generateText. And inside of here the model will be AI models.image. System will be system prompts.image.
So let's take a look. We are using the AI method from our AI package. And we are using OpenAI Chat GPT 4.0 Mini in my case because it's good at describing images or reading the text from images. You would use the equivalent of for Gemini, whatever it is, the base model. You can just try it, whichever is the best one, right?
You will see if it works or not. And once you have that, we also choose the prompt and we give it, you turn images into text. If it is a photo of a document, transcribe it. If it is not a document, describe it. So that's what we are doing here.
And then let's add messages here. Role, user, content, type, image, image, new URL, URL. And the AI will basically read this and it will give us result text back and just like that we develop extract image text and now every time user uploads an image we can extract or describe the transcript. Now let's handle pdf files. If mime type to lowercase includes pdf.
In that case, let's return extract pdf text passing url, mime type, and file name as the Third argument. Now let's develop extract PDF text function. So asynchronous function, extract PDF text. And let's open this. We're going to have URL, which is a type of string, MIME type, which is a type of string, MIME type which is a type of string, and add commas like this, not semicolons, and file name which is a type of string.
And we are going to return a promise which returns a string. And again, cons.result is a way to generate text. Model will be whichever model we defined to be used for PDF documents. The system prompt will be whatever system prompt we defined for PDF documents. Messages will be similar.
Role, user, and then content inside will be the following. The first content will be a type of file. Data will be new URL, pass in the URL, pass in MIME type, pass in file name. So we give as much context as possible to the system model. And then let's go ahead and add type text, text, extract the text from the PDF and print it without explaining, you'll do so.
So we're just adding some further instructions specifically here, because this will make the embeddings clearer. Because sometimes you can't exactly control AI models and they will add more tokens in the embedding that it needs to have. And then your AI model might give some wrong information. So that's why we are adding some additional instructions here. And then just do return results.result.text.
There we go. Now we have extract PDF function. And now let's go ahead and handle all the other text-based files. So if MIME type to lowercase includes text, Let's return extract text file content, passing the context, storage ID, bytes and MIME type. And just like we've developed the other ones, Let's develop the extract text file content here.
So asynchronous function, extract text file content. Let's go ahead and add context here and let's give it a type of storage, storage action writer. Then let's go ahead and give it storage id which is a type of id underscore storage, after that bytes which is a type of array buffer or undefined, after that MIME type which is a type of string. And as always, we are returning back a promise which resolves to a string. Now, let's go ahead and let's first define the array buffer.
This is going to be bytes or let's open parenthesis, await, open parenthesis again, await context storage get storage id. And then inside of here question mark dot array buffer and execute that. Storage.getStorageId and then inside of here ?.arrayBuffer and execute that. And you can see that now we have arrayBuffer or undefined returned. So basically we are either using the bytes that we are able to pass in if we got it through file upload or we are going to grab the storage file and attempt to run array buffer on it.
Either way it's either going to be array buffer or undefined. So if we are not able to get the array buffer let's go ahead and throw new error here failed to get file content. Now let's go ahead and do const text new text decoder decode array buffer. Now let's check if my type to lowercase is not text plain. In that case, let's run the AI model.
So what just happened here? Basically, we need to... There are two ways we can extract text. There is a, text files are fairly simple. So you can do that just by using the text decoder and the array buffer, right?
But What if you receive something that's technically considered a text file in the MIME type world, but isn't as simple as text plain, like markdown files? Well, in that case, we need to call a way to generate text. Why didn't I do it in here? Well, if possible, we don't have to use AI, right? And our goal as a business is to save money if we know how to do so.
In this case, we know how to extract text from a very simple text file using the array buffer. But just in case if it so happens that it's not text plain, so it's a markdown or something more complicated like HTML maybe, Let's go ahead and do the following. Result is equal to await generate text and then model AI models dot HTML system, system prompts dot html, messages, the first one coming from the user, content, the first content, type text and text in here and then we need to further describe this so type text, text, extract the text and print it in a markdown format without explaining that you'll do so. Like that. And then return result.text.
Otherwise, simply return the text from the decoder. This way, we are able to parse normal text files using the decoder, but also more complicated text files like HTML or markdowns. So we can still use the text decoder, but we need to analyze this more thoroughly by using AI. So we are giving the AI the content from the text decoder and then go ahead and do it even further. So we have the proper embedding And now this is working, this is working, and this is working.
Let's go ahead and handle the last type, which is the unsupported type. So throw new error, unsupported, MIME type. And let's render the mime type. This way we will know if user attempts to upload something that we don't support and we're going to get that inside of convex logs and inside of sentry logs as well because our convex and sentry are connected and you will be able to get the bottom of it by disabling that MIME type from being uploaded because it's not something we can handle right now. So if you've done this correctly, you shouldn't have any errors in this code.
Again, if you're unsure what to use here, just use whatever model you've used so far, and then you will see if it works or not. I recommend that you try this knowledge-based thing and the embeddings with very simple text files because they don't actually require AI. So you can just do the text decoder and it will work because it won't even have to use generate text. Perfect. We now have the extract text content file.
And now let's go ahead and import extract text content like this from lib extract text content and now that we have the extract text content we have to add those embeddings. So we now have the text as you can see. Basically whatever the user uploaded we have it in textual form. So now let's go ahead and do await rag dot add and I didn't import RAG so let's go ahead and just quickly do that. In order to add RAG you actually have to set it up.
So I didn't follow through the documentation entirely. So let's do that. Basically, instead of convex, instead of system, instead of AI. The same way we had the agents, now let's create a new file called rag.ts. Inside, let's go ahead and import your model again from your AI SDK and then whatever provider you are using.
Then go ahead and add rag from convex dev rag and then import components from generated api. Go ahead and define rag using new rag components dot rag text embedding model. Go ahead and add openai dot embedding. And now what if you don't know what is your embedding model for Gemini for example well you can go to aisdk and yeah by the way in the middle of me making this tutorial SDK version 5 was actually released. I'm not sure if it's fully compatible with the current convex versions.
So I would suggest that you actually use the exact version that I'm using or at least be on version 4 simply so you avoid any problems. But if you were been using version 5 so far and everything was working, no problem, no need to change it. And same thing goes for let me just see, AISDK OpenAI. Same thing goes for this. I think the versions kind of need to be similar here.
So what if you don't know what is your embedding model. So you go instead of providers on a SDK and you select your provider. In my case it's open AI. And if I scroll down here I think that I can find the embedding models. And in here you can see the embedding models that I have.
And for example, if you're using Google Generative AI, which I think it's Gemini, same thing. Scroll down and find if you have the embedding models. You do, perfect. And then just use Gemini embedding 001. I think that by default when you do open eye embedding you should see all the options here.
So if you're using Gemini and click Gemini dot embedding or Google dot embedding or whatever else you're using, type safety heel will help you. So I will use text embedding three small. Now we have a slight problem. Components that rank doesn't exist because we don't have our back end convex dev running. So just make sure you do through both dev.
So you have convex dev running. This will analyze the source code. It will see our new rag component. And let's just see. Okay, so components.rag now works, but this is still incorrect.
So let's finish it. Embedding dimension, I'm gonna add 1536. So I think it can do exactly the same. I think this is the same for all embeddings so it doesn't matter what model you are using. And let's export default rack.
You can read more about that settings in the actual convex agents rack and then in here you can find out exactly what it means. Let me try and find it somewhere. Okay, they have more in their RAG example. Basically, definitely research through this documentation here. Now that we have the RAG component, we can go back inside of files which we started developing and now we can import that rag from system ai rag in here let's go ahead now and do the following so rag dot add passing the context and then passing the namespace.
The namespace will be, where are these embeddings getting saved? Think of it like a role level security for the user who is currently logged in, right? Who should be able to access these embeddings? Because it will be quite dangerous if you don't pass the namespace, right? Namespace basically tells you, I should teach our AI something, but for whose organization should I teach that?
Think of it like that. So it's super important that you're passing the organization ID here. So only the user who is currently logged in and has a specific organization attached to them should be having these embeddings. It shouldn't be global. Imagine it's a super secret file.
You only want this to be visible for that organization namespace, right? Or imagine some random person can upload a file and then all of your customers suddenly have weird answers from your chatbot because it was thought that. So that's why namespace is super important to have. So let's add a little comment explaining that. Super important.
What search space to add this to? You cannot search across namespaces. And let's also do if not added it will be considered global which is something we do not want we do not want this add this comment so you know what this is about after that let's add the text let's add the key to be file name, let's add the title to be file name and now let's add metadata. The metadata will have the storage id so we can retrieve that file if needed, upload it by, if needed as well but let's add organization id here, file name again in the metadata and category will be category or null since it is optional and then let's add content hash which will be await content hash from array buffer and pass in the bytes. Why do we need this?
We need this to avoid reinserting if the file content hasn't changed. So if we upload the exact same file and we compare its hash from the array buffer, and we notice that it is the exact same file, we won't reinsert that into the embedding. So this is the exact line and comment from the convex example. Most of this actually is right from their rag examples here. So if you see that my comments here are exactly the same as their example series, because of that, I am teaching you exactly the way they teach to do this.
Great. So I think this is pretty clear what's happening, right? We basically had to extract text from various types of files that can be uploaded. And then we are creating embeddings using their reg.add. We are adding it to a certain namespace so that it cannot be searched by other organizations and then we are adding some metadata so that it's easier for us to display how this will look like later.
Now from here let's go ahead and let's extract entry id and create it. And now let's go ahead and check if not created meaning something went wrong. Let's console.debug entry already exists skipping upload metadata. And let's just do await storage context.storage delete storage ID. Like this.
And Finally, let's return the URL of the file we just uploaded will be await context storage get URL storage ID and entry ID. There we go. That is our add file function. Now let's go ahead and let's implement delete file function. Instead of being an action, delete file is going to be a mutation so export const delete file will be a mutation let's go ahead and add arguments here let's add the handler now let's define the arguments inside Let me just replace this with a comma.
The arguments are going to be very simple. We're going to be looking for EntryId. And that's going to be a type of vEntryId. You can import this exact type from convex dev rag. That's the only thing we're going to need.
After that, extract the context and the arguments from here. And now let's protect ourselves like we usually do by searching for our identity and the organization id so we can copy that from the add file action here. So make sure this exists. And of course let's import mutation from generated server So we don't have those errors anymore. After the user has confirmed their identification and their organization ID, let's go ahead and attempt to see if we have permission to actually delete this embedding.
How can we do that? Well, by searching for the namespace. Because remember, our embedding is stored under our organization namespace which besides meaning that we are the only ones who can search for it it also means we are the only ones who can delete it so const namespace await rag get namespace context namespace organization id. If there is no namespace let's go ahead and throw new error unauthorized. In fact, we can throw this.
And let's say invalid namespace. We don't have permission to delete or access this entry in the first place. Now let's attempt to get the actual entry. Now that we know we have access to this namespace by doing await rag get entry and passing the context entry id arguments entry id. If there is no entry it means this doesn't exist.
So let's go ahead and throw a new convex error here, not found, and let's simply say entry not found. Now let's check one more time if we have access to do this by using our metadata uploaded by so entry dot metadata question mark uploaded by be careful here this is not strictly typed so uploaded by is different from the organization ID, in that case, throw new convex error again and pass in the code unauthorized and the message invalid organization ID. So this is just the last fail safe because even though we confirmed our namespace, just in case something goes wrong and we just got this entry, we still have the entry metadata. If you remember in the add file, we add metadata which can be whatever, literally. This can be whatever you want.
And the one thing we added is uploaded by. So we upload on the level of organization. So only that organization should be able to delete this file. So this is just the final check. This also needs to match.
So this is also important for deletion. Mark that here. Make sure you didn't misspell uploaded by and make sure that when you highlight it down there it's highlighted up here so you know it's written exactly the same in the exact same capitalization. Make sure you're using the reverse value here so if it is not equal throw the error. And then finally let's go ahead and check if entry metadata question mark storage id is present what does this mean?
So remember when we upload a file the first thing we do is we upload it to the storage and then we create the embedding. So those are two separate systems. If you head to your convex dashboard You will see that right here. Instead of my echo tutorial here, you can see that I have my files. So my files are not my embeddings.
They are just files. I can literally add them from here. That will not create embeddings. But if I go inside of my data app and click on rag, you will see that this are the embeddings. So we need to delete both when the user requests deletion of a file that is tightly coupled with the rag.
So what we have to do instead of our delete file here is check if we have the storage ID which we also kept in the metadata. Make sure you are passing it here. So also important for file deletion. Make sure you have passed that here. If we have it here, let's go ahead and do await context storage dot delete entry metadata storage id as id underscore storage.
And you can import id from generated data model. So once you've done that you are finally ready to await and do rag delete context entry id arguments entry id and looks like this is deprecated oh okay use delete async in mutations delete async Let's go ahead and do it like this. Perfect. So that is how you delete an embedding. This is equally as important, right?
We don't want to infinitely fill our embedding and not be able to delete that embedding. This way, we tightly couple them together and we keep track of absolutely everything. Great. So we can't really test this, but what I want you to know is that if you have your convex functions ready, there's a 99% chance that you did everything correctly here and you should also have the rag available here right so if you have it available it means that your rag component was successfully added and it most likely means that you added the correct embedding model and everything. So even though in here I have added to add list files, I'm not sure how much sense this makes, right?
Simply because it's a long function and I think we've done enough for this one chapter and it will make more sense to create list files along creating the UI to render the list files. So I will actually leave this for the next chapter for that purpose because we already did a lot. So amazing job! You learned how to generate embeddings using the retrieval augmented generation component from convex. Now let's go ahead and merge all of these changes.
So 20 generating embeddings. Let's go ahead and stage all of these. Let's do 20 generating embeddings. That is the name. Commit.
I'm going to go ahead and create a new branch. 20 generating embeddings. And I'm going to publish this branch. And I think this will be quite interesting to see CodeRabbit review. Very interesting to see what it thinks of our code to extract text, what it thinks of our security measures.
Let's wait and see. And here we have the summary by CodeRabbit. We added support for uploading, indexing, and deleting files within organizational namespaces, including extraction of text content from images, pdfs and text files for improved search and retrieval. We integrated a retrieval augmented generation system to enhance AI-powered document search and embedding capabilities. We improved conversation handling by automatically escalating unresolved conversation when a new message is added from the operator side.
So let's go ahead and take a look at... We can take a look at how file upload works actually. That should be quite interesting here. So the user uses back-end add file to upload the file and passes in the file name bytes and MIME type. Then let's go ahead and see what happens here.
So once the add file adds this file to storage What we do is we analyze the file by calling the extract text content. We determine is it image, is it PDF, or is it text and then we return the text back. I think for this exact example when it differs between return extracted text or just return text is the specific example of do we have just text plain or is it a more complex text model and once the extraction of the text is complete we add the entry using the rack component. We confirm the entry via backend, and then we return back the final URL and the entry ID. And we already know the escalation method here.
So let's look at the comments here. In here, it suggested improving security validation and add support for more MIME types. So yeah, right now we only actually limit the types of images that you can upload. I don't think we explicitly limit the types of files you can add. So yeah, this could definitely be a good idea to limit the files that we can add because I mean, we already throw an error if there's an unsupported MIME type.
But yeah, maybe it could be a good idea to add them here and then throw before. We'll see. Yeah, I will consider this definitely. In here, it tells us to improve the error message so that we also mark the storage ID so he knows which storage ID is failing. That's a good idea.
Yes, for production that would definitely help us so we know exactly what keeps failing rather than just fail to get file which file right. Good idea. In here it mentioned a potential issue and I think I kind of thought of this when I was writing this because yes, if the file wasn't created, we debug with entry already exists, skipping upload metadata and then we delete the storage file here. So this is from the example from convex regs. So I'm not 100% sure why we're doing this, but I have to assume it is so we don't have any duplicates here.
That's the reason I think we're doing this. So perhaps we could either do an early return here or we could just not return the URL at all. We'll have to see the next chapter, our front end implementation and the upload dialogue to see do we even need this upload file because this is problematic, right? If we just remove the storage, this will be an error, I believe. So yes, that's what it's warning us about here.
Definitely correct here. And in here, interestingly, yes, it's telling us to consider atomic status update to prevent race conditions. I think there's several ways we can actually fix this. So here's a way they suggest. They suggest adding a new retrieval just before we patch this.
So a good idea. But we could also schedule this update for running in a separate thread I believe by using convex's scheduler. I think we can do that and then that will fix the transaction issue. I think that's the equivalent of transaction. Not 100% sure but I think it is.
I will consider this basically you can find it here scheduling, scheduled functions. Let's see this allows you to build powerful durable workflows without the need to set up and maintain queues. I think this could be used for that. I'll research if that's the correct way to do that. So yes, this could be a problem if you have a very active application and a lot of things are happening with this conversation, it could run into a transaction error or a race condition.
There is also a question of how convex handles this in the first place, right? So I can't tell you with confidence if this comment makes sense or not, but definitely good idea that we have to think about this. In here, It's actually teaching us a little bit about embedding dimensions here. So yeah, I have no idea what embedding dimensions are to be honest. And you can see how it knows exactly why I added this number, which I didn't know.
I just used the rag example. But in here it knows that it is because I'm using this model. So what does that mean for you? Well, I actually found out that if you go inside of AISDK, select your provider, find embedding models. If you scroll down, you will see that you actually have some default output dimensionality here.
So you can see depending on what you use, you can see the dimensions that you can put inside. Even though it says it supports custom dimensions, so I have to assume you can add whatever number you want inside. But if you're having problems in the next chapters, consider changing your dimensions to one of these numbers depending on the model you have added. So what does that mean? When you go inside of your rag.ts here, depending on what model you have here, Find it here and find the default dimensions.
Let's see if that's true instead of my open AI if I go inside of my embedding models Yeah, you can see if I use text embedding 3 small default dimension is one five three six The exact one I put here which definitely means that I can make it optional. No, it does not. Okay. But that's why I put that number here. Okay.
See, CodeRabbit is amazing. I would have no idea what I was doing. Great. So now I know how to add that the number of dimensions. So what should you do?
Because we didn't really test this out, whether this works or not. And I can pretty much put any number here, right? Find your provider. So whether that's Grok, whether that's DeepSeek or Google Gemini, I assume most of you will use because it's free. So Gemini has this amazing free model.
If you didn't know, that's why I keep mentioning it. It just so happens that I like using OpenAI. I have their API keys and I have the billing, I have the credits, so I prefer using it. And it's a reliable model for me. So if you're using Gemini, head into embedding models, scroll down here, find the model that you have put here and then find the default dimensions.
And then once you merge this pull request, don't do it here. So later when you merge this pull request change this. I will remind you in the next chapter. I will try my best to remember as well. Perfect.
So super helpful comment by CodeRabbit here to help us understand why we even added that. Let's go ahead and merge this pull request now and then let's go back inside of the main branch. Let's click synchronize changes. Let's click OK. Let's go inside of our source control, wait for the changes to synchronize.
Let's go ahead and open our graph and in here you can see how this looks like. So we merged 19, now we were in 20 and we merged that back here. I believe that marks the end of this chapter. Amazing, amazing job and see you in the next one.