In this chapter, we're going to implement background jobs into our project. Let's start by completing these three webhook events from our previous webhook implementation. So right now I don't have anything running and I am on my main branch. I'm going to go inside of source app API webhook route dot ds and the first thing I want to add is call session ended. So let's go ahead and add an else if here Event type is equal to call session ended.
And inside of here, I want to get the event as call ended event, which we can now uncomment, as well as these two. And we can get the meeting ID from call custom because this is a call event. This was a session participant event. So that's why we had to do a different way of grabbing the meeting ID. But in here, we can use our custom field and check if it's missing and throw an error.
And if it is actually finished and we have the meeting ID, we can go ahead and update that meeting to be processing and give it the ended at date. And only do that if we have a matching meeting ID and the meeting status is active. So in case this fires twice, we don't want to add new ended at date here. We can only do it once. So we are only doing it for a meeting which was currently set to active and now it's set to processing.
So that's one event down. Now let's do a transcription ready and call recording ready. So let's start with the transcription ready event. Else if event type is equal to this. So in here we have to do the same thing get the event payload as call transcription ready event and get the meeting ID using the call CID and split because of the specific format here.
And then in here, we're going to go ahead and we are going to update the meeting with the new transcript URL. So updated meeting and set transcript URL to be event call transcription dot URL, where we have a matching meeting ID here. And then I'm going to add to do call ingest background job to summarize the transcript Like that. So what we can do here for now is check if there is no updated meeting, and then we can simply throw an error like we did in the previous instances. Meeting not found.
And let's do a 404 here. And let's go ahead and do one more event. So we can actually copy everything from here. And let's do else if event type is call.recording underscore ready. You can go ahead and paste everything inside of here and this will be the call recording ready event meeting ID will be accessed the same way.
And in here, you won't have to actually return this at all. Just go ahead and update and set the new call recording URL. So these events will only fire the call recording ready and the transcription ready if in the meeting procedures you enabled the transcription to be auto on and recording auto on. Otherwise these events will not fire at all. So what I suggest you do now is go inside of your Neon Console, Tables, Meetings here and delete all of your meetings.
Make sure you have a clean slate here. Then go ahead and do npm run dev and npm run dev webhook. So you have a webhook running. Now let's go ahead and refresh and I'm going to create a new meeting here. Okay, so I just created a new meeting and I'm going to start the meeting and I will have just a quick conversation with the math tutor which should join any second.
And now that it has answered my question, I will simply end the call. And I will go back to meetings here and you can see that now this is actually set to processing because it actually captured my call session ended and it set it to processing here And what should be happening now is I should be receiving my transcription ready and recording ready events. And I should be able to see that in here. If it's finished, I can see the transcript URL and the recording URL. So in the transcript URL here, you can see what we talked about, right?
And in the recording URL, you will be able to see the actual recording of the event. Perfect, so both of these are actually working. Looks like we have successfully created recording ready and transcription ready events. Now that we have finished all of our webhook events, let's go ahead and let's implement a proper background job by configuring Ingest. So you can use the link in the description to let them know you came from this video.
And now let's go ahead and let's go inside of documentation, get started. You won't even need to create an account. So let's go inside of Next.js here. Make sure you select the app router. So in here I already have my app running so no need to do that but let's go ahead and let's install Ingest.
So I'm just going to go ahead and do that here and I will just add legacy peer depths so it doesn't fail with the installation. Now that I have installed it, let me show you the version. So 3.37.0 just for your information. And now let's run the Ingest dev server. So I'm gonna go ahead here and I will have it running the same way I have my webhooks running.
So alongside npm run dev, I have my webhook running and now I will have ingest-cli-dev. So if it asks you to upgrade, feel free to upgrade to use the latest version. This is the version I'm using for this tutorial. You can also specify that version here if you want to. And immediately upon running this, you will see that it is actually scanning my localhost 3000 for any kind of ingest functions.
And you can see how my npm run dev has a bunch of 404s because we haven't added any functions yet. But what you can do now is you can visit localhost 8288. Let me go ahead and paste that here so you can see it. Let me go all the way down. So you can go to localhost 8288 And you can actually see the Ingest dev server.
And inside of here, you will see all the functions that we create and all the steps and background jobs that we will be able to run. So now let's go ahead and let's actually create a background job. For that, we can simply follow the documentation. So let's just go back here. After we run this, let's go ahead and let's create the ingest client.
So I'm going to close everything here. I'm going to go inside of source and I will create a new folder called ingest. Like that. And inside I will create a client.ts and I will paste this inside. And I will call this meetAI2 simply because I already named one meetai so I don't want any conflicts just in case after that let's go ahead and create an api folder with the ingest route so this server will actually be able to find it and it won't have to scan for 404s.
Let's go inside of app API, let's create a new ingest folder and inside let's create a route.ts and let's go ahead and copy this and let's paste it inside and I will just change this to use our at sign. There we go. So make sure to put it inside of API ingest route.ts. It needs to be called ingest. And finally, you can now see this npx ingest CLI is hitting the correct route here.
So it finally found ingest in our project. And now let's go ahead and let's write our first function here. So I'm gonna go inside of ingest and create functions.ts and I will paste the hello world function here and I will import this from ingest client. So it will be called hello world here and you can see it accepts some data. It even waits for a second.
Now that we have this defined, we're going to have to add this back to our route. So let's go inside of the route here. And my apologies, the functions.ts files should be created in ingest folder here, next to the client, not in the API route. My apologies for that. So the API ingest should just have a route and the actual source ingest should have the functions.
Now let's go inside of the ingest route and inside of here let's add our hello world from ingest functions. And immediately upon doing that, if you go inside of your localhost, not 3000 but 8282, you will see in the functions a hello world function. And in here you can invoke it and you can pass email to be hello at mail.com and click invoke this function. And you can see you now have a run here, which first waits for one second and then says hello and your email. So that's how these background jobs will work.
But instead of triggering them from this dashboard, we are going to trigger them from actual code and they will simply work in the background. You won't have to await it. You can tell your user to log out and they can take a walk and this will still be completed without them. That's the power of background jobs. You don't have to worry about them timing out or erroring because these individual steps will retry themselves until they succeed or until you put a limit on retries.
So now that we have established this, we can go ahead and we can actually create everything we need. So let's create our own ingest function now. So the way we're going to do that is by going back inside of the functions here in the ingest. Now let's remove the hello world function and instead we are going to add export const meetingsProcessing to be ingest.createFunction. The ID will be meetings forward slash processing, and the event will be meetings forward slash processing.
And then we're going to have an asynchronous function which destructures the event and the step. And inside, we're going to start defining the steps we need to process our meeting. Now the first step here will be to fetch event data transcript URL. That's because if you go inside of our webhook route, when we call transcription ready here, we added a to do. Call the ingest background job to summarize the transcript.
So at the point that this function is called, we will have this stored inside of the updated meeting. So we are going to pass that along here as transcript URL and we can use ingest step.fetch as an optimized way to fetch in a background job. So no matter how long this takes, background job will take care of it, you won't have to worry about it failing or anything else. Now what we have to do is we have to parse this because this is currently in JSON-L format so it's not exactly workable within JavaScript by default. So what we have to do is we have to add npm install jsonl parse stringify.
And let's add legacy peer deps here. Once it's installed, you can exit. And let's go ahead and import it here. Jsonl from jsonl parse stringify. And in here, what we're going to do is call this a transcript and do await step.run.
And this will be a custom step called parseTranscript, like that. Let's open up an asynchronous method here. And first things first, let's go ahead and await the response, which we just fetched above, and turn it into a text. And then we will simply return JSONLParse and pass in the text, like this. The only problem is, currently, we don't really know the type that is waiting for us.
But thanks to this JSONL parse function, we can actually add the type. So let's go inside of our modules. Let's go inside of meetings and let's go inside of types. And down here, I'm gonna add a new type called a stream transcript item, including speaker ID, type text, start and stop. How do I know that these are the ones I need?
Because of our transcript URL, right? So I just visited my previous transcript URL, which we demonstrated to confirm it's working. And in here, you can see speaker ID, start timestamp, top stop timestamp, type, and text. So that's why I know that those are the things I need. So inside of here, I can now append stream transcript item from modules meetings types like this.
And now when I hover over a transcript, you can see exactly what I expect inside. Perfect. Now the problem is, I can't really do much just by having speaker IDs. We need to populate them and connect them to the users from our database. Quick interruption from a future me, just a heads up.
Right now we're using a step.fetch and this works fine locally during development but once we deploy, which will happen in a few chapters this line will break in production causing a malformed JSON object. The fix though is very very simple All we have to do is modify from step fetch to this. Basically, calling your normal fetch inside of a step.run. And then chain.then and turn the response into text. The problem now is that we also have to modify this line, but that's not a problem either.
Instead of using the response.text here, in this step, we're just going to go ahead and parse it and do nothing more. So this is what I would suggest you change your code to. Even though the code will switch back to the old version once I unpause this interruption, I recommend using this updated version going forward to avoid any issues later on. All right, now back to the current timeline. So let's add another step called transcript with speakers step dot run add speakers and inside of here the first thing we're going to do is we're going to establish all the ids from our transcript so we're going to be using a set to avoid any duplicates So all of them will be under speaker underscore ID.
And you can see how thanks to this type here, we have strict typing here. So we don't make a mistake. Just double check that you actually have a speaker ID here. You can find it here in your meetings. Just find a recording or transcript URL to check.
Great. Now let's go back to our app here. And once we have speaker IDs, let's go ahead and let's get all user speakers, because half of them will be agent speakers. So user speakers will need a database, it will need the user schema, it will need in array from drizzle ORM. So make sure you have imported the database, user schema, and drizzle ORM.
Great. Now that we have all of these, we have the user speakers. And we can now do the exact same thing, but for the agent speakers. But instead of calling users, we are calling agents here. So make sure you've added the agents schema.
Perfect. And now let's unify them all together in a constant called speakers. So we are combining the user speakers and the agent speakers here. And we are going to return transcript.map, get the individual item here. And now we're going to attempt to find the matching speaker ID with all of these speakers combined.
So we are going to load something, either an agent or a user. And then if we don't find any speaker, we are just going to return this existing transcript item and we are going to set the user name to be unknown because we cannot find that user in our database. Otherwise, let's return this existing item, but we are appending the user object with the proper speaker name so we know exactly who was speaking at that time. Perfect, and that is our job for actually adding the speakers to the transcript. Now we have to build an agent here.
So Ingest has their own agent kit which we can implement and then use as a step. So we can actually connect OpenAI to Ingest. But in order to do that we first have to go to our assets repository here and find a system prompt. You can use the link in the description to find this and go ahead and simply copy this entire prompt. Now go to the top of your meetings processing function here and create a summarizer and give it a create agent function and you can import create agent from another package that we have to install.
So let's just do that. Npm install ingest-agent-kit legacy-peer-depths. So after it's installed, let's go back here and let's import create agent, open AI and text message all from ingest agent kit. Inside of the create agent here, give it the name of summarizer. Again, give it a system prompt of the following.
Open backticks. And now you will simply copy the system prompt and paste it here. And then go ahead and simply add .trim here and add a model. And in here, add OpenAI. And we imported this from here.
So OpenAI, model, choose whichever model you want. I'm going to choose 4.0. And API key will be process.environment, OpenAI, API key. Always double check, make sure it's the proper one. There we go.
Perfect. So, we now have this, which means that we can now actually summarize this. So after transcript with speakers, we can go ahead and create another step using await summarizer dot run, give it summarize the following transcript, and then use JSON Stringify transcript with speakers because we now have them with speakers here. And once you actually have the output, you're going to add one more step, step.run, and add save summary asynchronous method and await database, update meetings, and set the summary here to be output first in the array as text message, which is our type we imported from ingest agent kit dot content as string and set the status to completed finally, and only do that where our meetings ID matches the event data meeting ID. Be mindful.
Oh, yes, let's import meetings from database schema, my apologies. And let's import equals from drizzle or M. So make sure you have added meetings here and equals from drizzle or m. Make sure you don't misspell the event data meeting ID because this can be anything. It's not strictly typed.
Same goes with event data transcript URL. All that's left to do now is go back inside of our webhook here, find the call transcription ready, remove the to do here, and do await ingest, which you can import from ingest client. .Send name will be meetings processing. So just make sure you don't misspell it, you can copy it from here, and then paste it here. And data and simply pass in the meeting ID and the transcript URL from this updated meeting which we've had right here.
Perfect. And we must not forget to now go inside of API ingest route and instead of Hello World add meetings processing here and you can confirm you've done it correctly by going inside of your functions here. Let's go ahead and refresh and you now have the meetings processing. But if you try it with this data, it will just fail. So the best way to test this out is to go ahead and have a new meeting.
So that's what I'm going to try out now. So I just started Math Consultations 3 and I will simply ask a question here and then we're gonna look if any runs will start automatically here. I'm now going to end the call which will now trigger the processing mode for that consultations here, and it will also wait for the webhook to hit. And once the webhook hits, you can see that we run the meetings processing function. So the first step was to fetch the transcript URL, which you can see right here, worked.
It has the speaker ID here and everything. Then we parse the transcript, so we turn it into JSON. Then we add the usernames. So this is me, John Doe, and this is MathTutor. So we use the speaker ID to find that.
And then finally, we used chat.gpt to summarize the transcript. And you can see that we have the content result here, and we saved that summary. And now, if I refresh my database here, I should see a summary text inside of here, and I will also see the status to be completed. So if I go back here, this now finally says completed and you can also see we have proper duration here. Amazing, amazing job.
So we now have everything ready to display the last components for this app. So now let's review and merge this pool request. I'm going to go ahead and open a new branch, 24 background jobs. Once I'm in a new branch I'm going to stage all of my changes and I will write a commit message. Let's commit and let's publish the branch and now let's review our full request.
And here we have the summary. So what I'm interested in here is the walkthrough. This update introduces ingest-based event-driven processing for meeting transcripts, adds new dependencies, and expands webhook logic to handle additional event types. We implement a function for AI-powered meeting summarization, and we update database records accordingly. We also introduced a new type for transcription items and some minor formatting changes.
So this is basically the goal of this chapter. Ingest-based event-driven processing, background jobs, something the user doesn't have to wait for and something that we don't have to await or be afraid that it will fail, right? The user can take a walk, the user can go away, that's the power of background jobs. They can simply do whatever they want while they wait. So Once the webhook reaches the proper event type, we update the meeting status and fields and we dispatch the meetings processing event with meeting ID and the transcript URL to ingest.
After that, we trigger the meeting processing function and we fetch the transcript URL and speaker info. We then send the enriched transcript to summarization to AI and we return the summary end notes and finally update and set it as completed. In here we do have some recommendations and this is not an SDK, SDK does not accept this kind of format so it actually gives you a suggestion even in that case. If AgentKit doesn't support this format at minimum use template literals. So it's basically suggesting us to use template literals instead of joining like this.
Basically to avoid potential prompt injection. So if you want to you can change this. I might change it in the next chapter. And the rest of the refactor suggestions are basically just error handling suggestions which Ingest does a pretty good job of just by itself so no need to do any of that we can just go ahead and merge this pull request and once you've merged it go back to main and go ahead and synchronize the changes like that and now inside of your graph here you should see that you have merged 24 background jobs. That marks the end of this chapter.
Amazing job and see you in the next one.