AI Engineer Summit 2025
Vercel AI SDK Masterclass: From Fundamentals to Deep Research
About this talk
Vercel’s Nico Albanese introduces AI SDK fundamentals, including local Node.js/TypeScript setup, generateText, GPT-4o mini, alternative providers, tool-assisted web access, and structured outputs. He then builds an agentic deep-research workflow that generates search queries, evaluates results, recursively pursues follow-up questions, maintains accumulated research state, and produces a Markdown report.
Chapters
- 0:00Workshop overview and local project setup
- 1:01AI SDK text generation, models, and web access
- 14:51Tool argument inference and structured outputs
- 24:52Designing the deep-research workflow and search queries
- 46:03Recursive follow-up research and persistent state
- 58:56Completed research agent and closing remarks
Talk transcript
- 0:00
Hey folks. My name is Nico. I work on the AI SDK at Vercel, and in this session today, we're gonna be looking at building agents with the AI SDK.
- 0:11
Now, this session is roughly divided into two sections. We're gonna start with a fundamentals section. This is gonna introduce you to the building blocks that you need to understand about using the AI SDK before we jump into building agents.
- 0:24
And then we're gonna be building a Deep Research clone, uh, in Node.js. So without further ado, let's get right into it.
- 0:34
To follow along, the first thing that you're gonna have to do is clone the repository here, install the dependencies, copy over, uh, any environment variables, um, and then you'll be ready to go.
- 0:45
In this project, we have just one file, uh, index.ts, and you can run this file by just running pnpm run dev. I have this alias to just pnd, so if you see me typing that, that's just running the script.
- 1:01
Great. So let's start with the first primitive that we're gonna be looking at, the generateText function. Now this is, as it sounds, a way for you to call a large language model and generate some text.
- 1:14
So let's take a look. In this session, rather than typing everything out line by line, I'm gonna be copying over snippets so we can get through things a little bit faster and focus on the core concepts rather than necessarily remembering to type everything out properly.
- 1:27
So let's start with the first snippet. What we've got here is a single function called main. It's asynchronous, and inside this function, we call generateText, which we import from the AI SDK.
- 1:43
We specify the model we want to use, in this case, OpenAI's GPT-4o mini, um, and then we pass in a prompt, "Hello, world." Finally, we log out the resulting text that is generated and call the function.
- 1:57
So we can head into the terminal, run pnpm run dev, and we should see a message back from GPT-4o mini: "Hello, how can I assist you today?"
- 2:08
Now, each of these generateText functions and, uh, streamText and generateObject and streamObject, as we'll see later, uh, can take in either a prompt as input or messages. And in this case, messages is just an array of messages where a message has a role and then some content.
- 2:26
So this would be the same as we had before if we change this to user.
- 2:33
For the rest of this session, we'll be using mostly the prompt key. So one of the core features of the AI SDK is its unified interface, and what that means is that we're able to switch between language models by just changing one single line of code.
- 2:49
Now, there are many reasons why you might want to do this. It might be because a, a, a model is cheaper, faster, um, better at your specific use case.
- 2:59
Um, and speaking of better, one thing that we can try asking this model in particular is, uh, something that we know it might struggle with, like when was the AI Engineer Summit in 2025?
- 3:18
Now, I know for a fact that GPT-4o mini is not going to be able to do this because it doesn't have access to the web, and its training data cutoff is somewhere in 2024.
- 3:29
So we can see, "I'm sorry, but I don't have information about events scheduled for 2025, including the AI Engineer Summit." So how could we solve this? Well, we could, and we'll look into later, add a tool, and that tool could call the web and return those results and pipe those into the context of the conversation, and then
- 3:49
the language model can deduce from there. But we could also just pick a model that has web search built in, something like Perplexity. So how do we change to a different model?
- 4:01
Well, all we have to do is change the model that we specify here to Perplexity, invoke that model provider instance, and then select the model that we want to use.
- 4:14
So in this case, we've imported a new, uh, provider, this time from ai-sdk/plexity, uh, and then we specify we want to use Sonar Pro. So if we run this again now,
- 4:29
the model will come back, "The AI Engineer Summit in 2025 took place from February 19th to February 22nd, 2025 in New York City." This is right.
- 4:39
One thing that's interesting here is that unlike the OpenAI response, because Perplexity is using sources, it's actually named them or referenced them in its response. How do we access them?
- 4:52
Well, the AI SDK makes this accessible with the sources property. So we can save, run the script again,
- 5:05
and we'll see the sources inline. Very, very, very cool. And this isn't just limited to Perplexity. We support a ton of providers, and you can check all of them out at our documentation.
- 5:16
If you head to ai, uh, sdk.vercel.ai and head to our providers page, you can see we have a whole host of providers here, um, many of which support web search as well.
- 5:28
So if, for example, you wanted to use, let's say, Google's Gemini. So we can import the Google provider here and use Gemini Flash, uh, 1.5, and then specify that we want to use search grounding.
- 5:44
Save, run the script again, and we'll see this same prompt with the same configuration going off to a different model at a different provider and getting what we hope is a similar accurate answer.
- 5:58
"AI Engineer Summit took place from February 19 to 2025," and a bunch of sources inline to confirm this. Really, really cool and really powerful stuff So that was the first primitive that we looked at, something as basic and simple as just generating text from a language model.
- 6:14
But what if we want to go beyond generating text and use language models to interact with the outside world and to perform actions? This is where tools or function calling comes in.
- 6:25
While tools may seem complicated, at the core, it's a very simple idea. We give the model a prompt, and then we also pass as part of the conversation context, a list of tools that it has available to it.
- 6:38
Each of these tools will be provided with the name of the tool, as well as a description of what the tool does, so the model knows when to use it, and finally, any data that it requires in order to use those tools.
- 6:52
Let's say the model decides it needs to use one of the tools to solve the user's query. Rather than generating some text as a response, it would generate a tool call, meaning it would generate the, the name of the tool it wants to use, and any arguments or data that it can parse from the context of the
- 7:11
conversation necessary to run those tools. It's then on you, the developer, to parse that tool call, run that code, and then do with that as you please. So how do we do this with the AI SDK?
- 7:26
Well, let's check out a very simple example, and when I say simple, I mean simple. We're going to ask a language model to add two numbers together. So copy in some code here.
- 7:36
Uh, we've got our same main function as before. We're calling generateText again. We're specifying our, our model as gpt-4o. Our prompt this time is, "What's 10 plus 5?" Very difficult question.
- 7:48
But this time, we are passing a tool. Now, to pass tools to the generateText or streamText function, you pass a tools object. Within that object, you specify a key or the name of the tool, in this case, addNumbers.
- 8:03
Then you can, as the value, use this tool utility function, which I'll explain in a second, to then, uh, define what the tool should be. So the tool is going to have a description.
- 8:14
This is what the tool does, and this is really important because this is what the language model uses to decide whether it should invoke that tool. Uh, parameters, this is the data necessary for the language model to run this tool, and this is what the language model is going to have to parse from the conversation context in
- 8:31
order to use the tool. And then you have the execute function. This can be any arbitrary asynchronous JavaScript code that will be run when the language model generates a tool call.
- 8:43
And what this tool function does here is this is completely unnecessary but is a really nice DX, um, improvement here. And what it does is that it provides tape- type safety between the parameters that you define here and the arguments that, uh, come into your execute function.
- 9:02
So to showcase this, if we change this to string, if I can spell. You can see I can't spell. Um, but we can see that numOne is still a number, where numTwo is a string.
- 9:14
Uh, so again, this is not necessary at all, um, but it does make building and, uh, working with these tools a lot easier. Now, what's going on behind the scenes is that the AI SDK is going to parse any tool calls, and then it's going to automatically invoke the execute function and return that in a tool results
- 9:35
array, as we can see right here. And that's what we're logging out to the console. So if we head to the console, clear it out, and run the script, we should see a tool result logged out to the console, and that indeed we do.
- 9:49
We see toolResult, uh, addNumbers. We see the arguments that are parsed from the conversation context, and then we see the result itself.
- 10:00
But notice that we have logged out the tool results this time, and instead of, instead of the, the text generated. And, and I had said before, it's kind of a n- nuanced thing here, but the language model isn't generating text anymore.
- 10:15
It's generating a tool call. And we can see that if we said console.log, actually, we just delete this all together, and we log out the resulting text,
- 10:25
we won't see anything in the console. It's completely empty. So how can we get the model to actually incorporate the tool results into a generated text answer, right? We want it to synthesize this action, whatever it's performed, and communicate that back to the user.
- 10:44
We could look at the last tool result, so const lastToolResult equals result.toolResults, and we pop, we take that last one, and then we say if lastToolResult equal, uh, dot, .name, .toolName,
- 11:08
uh, equals, and it's cool we've got full type safety here. Um, and then we can say, oh, maybe we want to await and, and generate text further and return that to the user.
- 11:19
But as you can see, this is error-prone. I've made, like, three mistakes already. But also, it doesn't scale, right? If we add 10, 15, 20, 100 tools in here, we don't want to write out 100 of these conditional statements.
- 11:32
So what we can do instead is use a property called maxSteps,
- 11:39
and we're going to set this to a number. Now, maxSteps can seem, again, a little bit confusing, but what it is at its core is that if the language model decides to generate a tool call, and therefore there is a tool result, we are going to send that tool result alongside the previous conversation context back to the
- 12:01
model and trigger another generation. And the model will continue doing this until it either generates just plain text, i.e., there's no tool result, or we reach the threshold for the maximum number of steps.
- 12:15
So this is quite a, a, um- A, a simple but powerful way to allow the model to keep running autonomously, picking the next step in the process, um, without necessarily having to add any logic or rerouting, repiping outputs here.
- 12:36
So if we run this code now, and, and what we're gonna do is we're actually gonna log out the result.steps.length, and actually, we could even log out the whole
- 12:50
steps so we can see what's really happening. And we're gonna stringify this so it looks nice. Uh, we should see that the language model is going to respond to our--
- 13:01
If we go up all of the, the crux, uh, we can see ten plus five equals 15, so it responded with plain text. And now we can see the different steps.
- 13:10
We've got the initial step, which generated a tool call, add numbers ten and five, and then we can see the, the, the tool result that we had logged out to the console, a bunch of, uh, stuff here.
- 13:23
Uh, cool note that you can tap into literally the, the raw request and response body with the AI SDK, so you can see exactly what's being sent to language models if you ever need to d-debug, but that's aside.
- 13:35
Um, and then we should see the second step. Uh, and the second step was step type tool result, um, and we can see that there is text actually in this, uh, generation.
- 13:49
So cool. We've now seen how we can build what we're gonna call a multi-step agent, because that's what this is. This, this language model has been given the ability to choose the number of steps that it wants to use and choose the, the tools or the direction it wants to go down.
- 14:04
But obviously, having just one tool here doesn't showca- doesn't showcase this very well. So why don't we add another tool here to see this agentic behavior in action? What we're gonna do is, again, I'm gonna just copy over some code.
- 14:19
We are going to add a tool that has the ability to get weather. Description here, kind of obvious, get the current weather at a location. The parameters here, we need the latitude, longitude, and city.
- 14:34
Um, and then finally, in the execute function, we're gonna make a fetch request, passing in the latitude and longitude, and then we are gonna unwrap the weather data and return it in the, in the tool result.
- 14:45
Now, a lot of people m-might be, uh, thinking in their head,
- 14:51
"We're not providing the latitude, and we're not providing the longitude. Like, is this gonna fail?" And this taps into a really cool, uh, inference capability that we can use, uh, in these parameters in that we can let the language model infer these parameters from the context of the conversation.
- 15:10
So we're fairly certain that the user is going to give city as, as part of their prompt, and in this case, we're gonna be asking for the weather in two cities, and then we're gonna want to add them together.
- 15:23
So we'll know that we'll have the city, and we'll let the la-- the language model use its training data to effectively infer what these might be. Now, I wouldn't suggest doing this particular example in production, but there are a lot of really cool use cases that you can u-use this pattern with.
- 15:42
So I foreshadowed a little bit. Our prompt here is gonna be, "Get the weather in San Francisco and New York, and then add them together." So we have two tools available here, getWeather and addNumbers, and we have a maximum number of steps of three.
- 15:55
So we'd expect probab-- Well, actually, let's not even guess. Let's see. We can, um,
- 16:03
print out the steps, the length of the steps, uh, the steps themselves, and then, uh, the resulting text generation. So I save, run the script,
- 16:16
and we'll see a response. The current temperature in San Francisco is twelve point three degrees Celsius. In New York, it's fifteen point two. When you add these temperatures together, you get twenty-seven point five degrees Celsius.
- 16:27
So let's see how many steps we had. Three steps. We had an initial step, which was a tool call step. Uh, we didn't unwrap this in the, in the, in the console log, but I know exactly what these are, 'cause I have done this demo before.
- 16:42
Uh, and this is cool. We're using parallel tool calls here. Um, so the... Actually, we can see in the body. We can see in the body right here. Um, uh, or it's probably gonna be in the, in the response actually, which is also wrapped in, in here, not unwrapped.
- 16:58
Uh, but what's happening here is that we're doing our getWeather, our, our getWeather call twice. We're doing for New York and then San Francisco. And then in our second step, we're gonna be doing our add two numbers together.
- 17:12
And then for our final step, we actually, uh, have the, the text generation itself. Um, and I can even, if we do it again here, just to show you, uh, let's stringify this out.
- 17:26
Um, just in case people don't believe me, we'll do it one more time, and this time we'll see exactly what is, what is coming out.
- 17:38
So if we jump, it's good, good, good sign that we're getting exactly the same response. It means that the data is correct. Um, and if we scroll up to the, where were we?
- 17:48
The first step. So three steps here, and yeah, you can see. So we've got two tool, tool calls. We've got getWeather for, uh, the latitude and longitude and the city of San Francisco.
- 18:01
The same for... Oops, scrolled too fast. For New York, and then we have the add two numbers step.
- 18:12
If we get there, let's scroll down. There we go. Add numbers, number one, number two, we get our result, and then finally, we have our
- 18:27
Actual text generation, which comes at the very end. So awesome. That's two major fundamental building blocks out of the way, generating text and using tool calls. The final thing that we're gonna look at is generating structured data, also known as structured outputs.
- 18:43
There are two ways to generate structured outputs with the AI SDK. We can, one, use generateText with its experimental output option, which we'll look at first, and two, which we'll be using more later in the session, using the generateObject function, which is a function dedicated to structured outputs.
- 18:59
The generateObject function, uh, I will, I will tell you, is my favorite function in the entire AI SDK, absolute workhorse of a function, and we'll see it later. So let's take a look at our existing tool calling example, where we're getting the weather in two locations and then finally getting the sum of those two numbers added together,
- 19:18
and see how we could add structured outputs to make the eventual output easier to use maybe later on in our program. So the way that we're gonna introduce structured outputs here is using the experimental, I think we have to head up here, the experimental outputs, um, output key.
- 19:38
We can pull in output.object from AI, um, and then we define in here our schema.
- 19:47
We're gonna be using Zod to define our schema, and if you've never used Zod before, it's a TypeScript validation library that is super powerful and, uh, particularly paired with the AI SDK, seems like a match made in heaven.
- 20:00
We'll be looking at it a lot in this session, and it makes working with structured outputs an absolute breeze. So let's define what we want our output to actually be.
- 20:10
In this case, we know we want the sum, which is gonna be a number, so let's define a key of sum and make it a z.number. Finally, we add our comma,
- 20:21
and now instead of logging out all of this, all of the steps, and instead of logging out the text, I'm just gonna log out the experimental output. And let's run the script and see what we get.
- 20:36
So this time, instead of getting all of that extra text, which we maybe could have prompted away, we just have a simple type-safe object that we can access. In this case, we've got experimentalOutput.sum, which we know is going to be a number, otherwise it will throw an error.
- 20:53
So you can combine tool calling with structured output to build out some really, really awesome, uh, use cases. Now let's very quickly look at the generateObject function. So I'm going to delete everything in our main function, uh, and copy over a generateObject example.
- 21:10
So we'll save, we'll clear everything away. And in this case, uh, we're gonna be looking and seeing if AI can help us with a very in vogue problem, and that is defining what an AI agent is.
- 21:25
I know Simon, um, had, had posted a, a survey on Twitter asking for definitions and had crowdsourced something like 250 different definitions, which ended up being six categories of different definitions.
- 21:42
Anyway, nobody can, can agree on it, and so let's see if AI can help us. Uh, spoiler alert, I don't think it will. But, so what we've got here is our main function as per usual.
- 21:53
We're importing generateObject from the AI SDK. Uh, we specify our model as gpt-4o-mini. This should start to look quite, uh, uh, similar to how we've been doing it previously.
- 22:04
Pass in a prompt, "Please come up with ten definitions for A- AI agents." And now with generateObject, we can define a schema. In this case, our schema is going to have one key.
- 22:14
It's going to be an object. That key is definitions every, um... And this is defined as an array of strings, and then that will give us a type-safe resulting object that has our definitions key on it, which is an array of strings.
- 22:29
So if we run the script, wait a second, we should see 10 probably pretty bad definitions of what an AI agent is. "An AI agent is a software entity that uses artificial intelligence techniques to perform tasks autonomously or semi-autonomously."
- 22:48
Not as bad as I thought, but why don't we see if we can alter this slightly? And, and the way I wanna do this is provide some more details to the language model of exactly what I want it to do for each of the strings in this array.
- 23:05
Now, we could jump into the prompt and say, "Each of these definitions should be X," but one of the really, really cool features we can tap into with Zod is the .describe function.
- 23:18
And with .describe, we can, as it sounds, go to any key or any value, sorry, and we can chain on this .describe function. And in here, we can describe exactly what we want for that exact value.
- 23:32
So I'm gonna say something, uh, kind of cheeky, and I'm gonna say, um, I'm just gonna paste this in. "I want the language model to use as much jargon as possible.
- 23:45
It should be completely incoherent." So let's see what this can do. Spoiler alert, this is how I spend a lot of my time hacking on, on language model stuff, just, like, asking really ridiculous stuff.
- 23:58
And so let's see what we got here. "Autonomous entities that leverage algorithmic heuristics to optimize decision-making processes in dynamic environments."
- 24:07
That is complete BS, um, and we've got 10 of them. [chuckles]
- 24:13
So, um, so this is the really cool thing about working with, uh, structured outputs with the AI SDK, and particularly with that Zod integration. It makes defining these schemas and providing context super simple and really easy to maintain as well Great.
- 24:30
And with that, we've gone through the fundamentals of building agents with the AI SDK. Now we're gonna move on to a more practical project. We're going to be trying to build a Deep Research clone.
- 24:44
Now, obviously, this isn't going to be a full crazy application implementation. We're just gonna be doing it in Node, so we're gonna have a terminal script that we're gonna build.
- 24:52
We're gonna pass a query, and then we're gonna conduct a bunch of deep research and write a, a markdown report into our file system. But in doing this, this should introduce us to a few things.
- 25:04
It should introduce us to, one, how we would break down this idea of, of deep research into, like, a structured workflow. Um, and within this workflow, uh, we're gonna have some autonomous agentic elements.
- 25:17
So we'll see what a production AI system might look like. Um, and we'll also look at how we can combine different AI SDK functions together to build these more complex AI systems that can really cater to interesting and honestly cool use cases that you can use in production to help build cool stuff.
- 25:41
I don't know how, how better to, to, to say it than that. So without further ado, I'm gonna head to the Deep Research section of the companion site, because I've provided a nice explanation of how the workflow is gonna work in, in natural language, and also a, a nice diagram to explain exactly what's going to be happening.
- 26:02
Now, also, if you haven't used Deep Research before, one, I highly suggest you check it out. Um, OpenAI has a version. Deep Research-- Uh, Gemini has a version as well.
- 26:10
But roughly speaking, what's happening is you give these products a, a, a topic to think about, and it will go off for an extended period of time searching the web, aggregating resources, going down, like, webs of thought, and then it will aggregate all of that information together and finally return it into a report to hopefully solve your
- 26:31
query. And the cool thing is this really taps into a fundamental, a, a, a fundamentally strong part of, of language models of this, like, synthesizing troves and troves of, of information.
- 26:47
Um, so yeah. Let's look at how this workflow is gonna look like. So the rough steps are gonna be, we're gonna take an input, uh, or a rough query, a prompt.
- 26:57
We're then gonna, for that prompt, generate a bunch of sub-queries. So let's think about, like, we wanna do research on electric cars. Uh, the queries or the search queries that we might generate are, like, what is an electric car?
- 27:10
Who are the biggest electric car producers? And so on. Then for each of those queries, we're going to search the web for a relevant result, and then we're gonna analyze that result for learnings and follow-up questions.
- 27:24
And then if we wanna go into more depth, which we'll look at in a second how that works, uh, we will take those follow-up questions as well as, like, the existing research, generate a new query, and, like, completely recursively complete that process, meaning, like, we basically start again while keeping all of the accumulated research.
- 27:46
And in this way, we can go down these, like, these webs of, of thought and of questions and, and, and build a, a comprehensive, aggregated... A comprehensive set of information about a, a given topic.
- 28:01
So what this might look like in theory, I like this little explanation. Uh, so let's say we have electric cars. We're calling this level zero, just like this is the initial query.
- 28:11
And at level one, uh, we're gonna have a, a breadth. Now, breadth is just, like, how many different, uh, queries do we want at each step. So let's say we're at s- we're at level one, um, and we're doing electric cars, and the three search queries that we generate are Tesla Model 3 specification, electric car charging infrastructure,
- 28:32
and electric vehicle battery technology. And then for each of those different queries, we'd complete the research, and maybe for Tesla Model 3, we would then start generating, uh, go down the web, have two breadth.
- 28:47
Uh, like, we wanna generate two different, um, lines of inquiry to go down, and then we go down. Okay, we wanna know Model 3 range capacity and Model 3 pricing.
- 28:58
And then for electric car char- car charging infrastructure, we wanna know fast charging stations in the US, and home charging installation, and so on. And so depending on what depth and breadth settings we set, um, we're able to control the level of information, um, and the depth of information that we gather.
- 29:22
So that was a lot. Let's, uh, let's jump into actually trying to build this, and the first thing that we're going to look at is building a function that can generate some search queries.
- 29:34
So I'm gonna dive back into the, back into the code editor. We're gonna clear out this file, and we are gonna copy in our first function. So the first thing, like we said, we wanna generate a bunch of search queries.
- 29:49
This is going to be a single function called generateSearchQuer-Queries. I'm very creative. Uh, it's gonna take in a query, uh, which is of type string, and the number of search queries that we actually wanna generate.
- 30:01
We're gonna set this to be three just as default. This is for you to play around with later if you'd like. Um, then we use the generateObject function. We're specifying a mainModel so that we can just, uh, set it once, reference it everywhere, and if we wanna update it later, we can in just one place.
- 30:19
So that's why we're doing that. We have a prompt here. Uh, we, we use template strings all over the place in this, and you'll probably end up using them a ton.
- 30:27
Super helpful, uh, for this use case. So we say, "Generate n number of search queries for the following query," passing in the, the search query. And then we ask for a structured output, which should have a, uh, an array of strings, minimum of one, maximum of five, although we're setting it kind of loosely here at, uh, three.
- 30:49
And then we return those queries So that's our generateSearchQueries function. If we go back to our companion site, we see the next thing that we have to do is map through each of those queries and search the web for relevant results, analyze the result for learning and follow-up questions, and then follow up with new queries.
- 31:09
So let's go one at a time. The first thing actually we'll, we'll want to do is have a main function so that we can actually, uh, specify a prompt and then call this generateSearchQueries.
- 31:21
So our prompt is gonna be, if you were at the AI Engineer Summit, there's an awesome talk, and actually, uh, it's, I think it's available online, from the Gemini team where they walked through, uh, how they went about building Deep Research.
- 31:32
It was a really, really good talk, and one of my favorite things from it was hearing the, the prompt that they used as, like, the, the kind of the gold standard to evaluate the progress of the, the product, and that was:
- 31:47
What do you need to be a D1 shot put athlete? So in that similar vein, we're gonna be using that today.
- 31:54
So we take that prompt, and then we pass it into our generateSearchQueries function. Uh, because we specified a default number of search queries as three, we, we don't need to specify one here.
- 32:06
Uh, and then that's going to return our queries. So we can do just a little check-in to see what's happening here and log out our queries. Run the script,
- 32:17
and we should see fairly quickly we've got three potential search queries for our prompt of "What do you need to be a sh- D1 shot put athlete?" These are obviously geared for a search engine, so, like, "Requirements to become a D1 shot put athlete," "Training regimen for D1 shot put athletes," "Qualifications for NCAA Division One shot put."
- 32:36
So three pretty good queries that I think would make sense to, to look for if you wanted to learn more about this. All right. Back to our companion site.
- 32:46
We'll see the next thing that we need to do, map through these queries and search the web for a relevant result. First thing that we're gonna implement now is a function to actually search the web for relevant results, and the service we're actually gonna use today to do this is called Exa.
- 33:02
Uh, if you haven't used it before, highly rate it. Um, [lip smack] uh, really enjoy using it. It's fast, cheap, um, but judge for yourself. Like, we'll use it now and, uh, I think they've got a great API.
- 33:16
And so yeah, let's build a function to search the web with Exa.
- 33:23
So I'm gonna copy over some code. We'll head back to, uh, our code base, and we're gonna make a few new lines here. Uh, so what we're gonna do is we're gonna import Exa from the exa.js package.
- 33:33
We're gonna instantiate Exa, Ex- Exa, Exa, passing in our, uh, Exa API key. Uh, we're gonna define a type which we're gonna be using heavily later, and then finally, we are going to actually search the web.
- 33:48
So our searchWeb function takes in a query, which is type string, and then it's going to use the Exa searchAndContents function, uh, passing in our query and then specifying some optional config.
- 34:02
Two optional config we have here is, one, the number of results we want. I've left this as one, just as this is a simple demo, but you'd probably want to set this as configurable and allow the language model to infer what is necessary based on maybe, like, how complicated something might be.
- 34:18
I don't know. That's something for you to experiment with. Um, and then the second option is liveCrawl. Um, and liveCrawl is kind of as it sound, allows you to, or ensures that the results that you're getting are live, uh, rather than something that's in their cache.
- 34:34
So you obviously take a little bit of a hit on performance or time for this, uh, result to be executed, but you're sure that everything that is executed is, is, is live, is up to date.
- 34:47
Now, the other thing that I'm doing here is that I'm actually mapping through the results and only returning the information that I feel is relevant or necessary for completing this process, and there are a few reasons for doing this, but the main reason is to reduce the number of tokens that I'm sending to OpenAI.
- 35:07
Um, two reasons for that. One is that it's cheaper. Fewer tokens, uh, just going to be cheaper. Um, but second and more important, I found that the language model is so much more effective when you trim away all of the irrelevant information.
- 35:23
So, um, things like a favicon. Favicon link is going to take up a decent amount of space, not necessary at all for the language model to actually use these resources.
- 35:34
So I tend to do this a lot whenever I have tool calls or, or building out tools for working with language models, ensuring that the information that I'm returning or providing as part of the context is entirely relevant to the generation or to, to the task at hand, we should say.
- 35:54
Cool. So that's our searchWeb function, and now if we go back to, again, our... We can think of this a bit like a checklist. The next thing that we're gonna have to do is analyze the results, um, the search results for learnings and follow-up questions.
- 36:10
Now, this is gonna be the most complicated part of the entire workflow, and this is also gonna be the agentic part of the workflow. And so what we're gonna do is we're gonna use generateText as we did before, giving it two tools.
- 36:22
We're gonna have a tool for searching the web, and then we're gonna have a tool for evaluating the relevance of that tool call. And this is kind of... I, I say this is the most interesting and equally also the, the agentic part, um, because it's going to continue doing that flow for as long as it takes to
- 36:42
get a, a relevant search result. So let's see how we can implement this. I'm going to copy over this code, and bear with me 'cause there's a lot. There's a, a decent amount of code here, uh, but we'll walk through all of it and see how it works So I'm gonna also, uh, make sure that we've got
- 37:01
our imports all good. I'm gonna close off, uh, some of our tools, and now we're ready to go. So this function is called searchAndProcess. It is going to search and process.
- 37:14
It's going to take in our query. Uh, again, remember, search the web for a relevant result and analyze-- uh, search the web for... Map through each query and search the web for a relevant result.
- 37:25
So we're taking in that query. Uh, we're gonna take-- we're gonna create two local variables for this, for this function, uh, pendingSearchResults. And if you can think about it, like this process of searching the web and trying to figure out if it's relevant, when you search the web, you're gonna add that result to the pendingSearchResults array, and
- 37:44
then for evaluate, you're gonna pull out whatever the most recent, uh, pending result is, check if it's relevant. If it is, pop it into the final searchResults array. If it's not, just discard it.
- 37:57
Um, so that's the rough flow that we're gonna be building here. We use our main model as the model here. Our prompt is very complicated. Search the web for information about our query.
- 38:08
Uh, we give it a system prompt. "You are a researcher. For each query, search the web and then evaluate if the results are relevant and will help answer the following query."
- 38:19
We then pass in the query, which is right here. I was looking for it below. It's right above. Um, we set our max steps, so we want this agentic loop to run up to five times.
- 38:33
You could set this higher, and we can set this higher for now, um, and that will just allow that process of finding a relevant link to continue onwards and onwards.
- 38:43
I like to keep these relatively low just to ensure that it doesn't go off the deep end, but experiment, experiment, experiment is my, like, main advice for building with, with these tools.
- 38:55
Our first tool that we have here, I'm saying tool a lot, uh, is the searchWeb. This searches the web for information about a given query. We take in a query which is a string, uh, our string, uh, we then pass into the searchWeb function that we just created earlier.
- 39:10
Uh, we get back some search results, that type we declared, uh, at the beginning, which I can show you here, this type right there. Um, and then finally, as I was saying, we, we take this local variable, this pendingSearchResults, and we push whatever these new, uh, searched web results that we get, and we also add those to
- 39:33
the, uh, conversation context by returning them from the tool. And then we have the evaluate tool, and this is to evaluate the search results. It doesn't take in any parameters.
- 39:45
Um, and in the execute function, we are going to pull out the latest pending result, uh, then run it through generateObject, asking it to evaluate whether the search results are relevant and will help answer the following query.
- 39:58
Uh, and then we pass in with this XML syntax our stringified pending result.
- 40:06
Now, uh, the final thing here with this generateObject, rather than specifying the schema with Zod, uh, because we just wanna know whether it's relevant or irrelevant, we can actually use generateObject's enum mode to specify just the two values that we need.
- 40:20
Something's either gonna be relevant or irrelevant. Uh, this is super ergonomic. You could use Zod here. I just find this very easy, particularly if you have just two values.
- 40:30
Um, and it's also easier for the language model when it's literally restricted to two model-- to two values rather than generating a full structured object.
- 40:40
Uh, then if the evaluation is relevant, uh, we will push the pending result to the final result because we know that it's now relevant, so that's going to be a good source that we want to keep.
- 40:52
Uh, we have some logs here. And then this is interesting here. We say if the evaluation is irrelevant, we return the following string from this tool call: "Search results are irrelevant.
- 41:03
Please search again with a more specific query." So when max steps triggers the next generation, the language model is going to see the most recent step that took and the last part of that most recent step, i.e.
- 41:16
the tool result, was, "Please search again with a more specific query." So we have this system that is repeating, um, but with feedback based on what's going on. The last thing to mention here is the fact that I did not use the parameters here to parse out the search result, and, uh, there's a very, uh, specific reason
- 41:37
for doing that. Search result can be super long, right? This could be a whole entire crawled web page that could be, I don't know, 10,000 tokens, and we don't want the model to have to actually parse that out.
- 41:49
It's literally generating the text that already exists in the context above. One, costs money. Two, it takes a long time. Three, it's probably error-prone because it's literally just writing out stuff that's above.
- 42:04
It's not referencing it. And that's why I like to do this, um, these local variables within this function faster, cheaper, more accurate, and so on. So this is the searchAndProcess function, and we can add it to our main function and see how it works.
- 42:25
So we're gonna console.log our searchResults and see, see what we got. So we'll clear this out, run the function again.
- 42:36
"Searching the web for requirements to become a D1 shot put athlete.
- 42:41
Found Throws University benchmark lifts to throw 50 feet from school shot put. Evaluation completed. It's irrelevant."
- 42:53
So it found a new one, considered it irrelevant.
- 43:06
Found another one, men's track recruiting standards, and considered that irrelevant, or considered that relevant, sorry, and then it returned a bunch of... Oh, boy, we returned the whole result.
- 43:16
So this is, like, the whole scraped, uh, webpage in here. So we can see, very cool, it is working.
- 43:24
Great. So let's move on to the next step, and what is the next step? Uh, the next step is to analyze the results for learnings and follow-up questions.
- 43:36
So to do this, we're going to, again, create a new function. This new function is going to be called generateLearnings. It's going to take in a query, which is a string, and then, uh, the search result itself, so this is, like, the, the webpage and, and the scraped content.
- 43:52
We're then going to use, uh, surprise, surprise, my favorite function, generateObject, using, uh, our main model again and providing a prompt here. This time the user is researching query.
- 44:03
The following search results were deemed relevant. Generate a learning and a follow-up question from the following search result. We use those XML tags again to nest in our search result that is stringified.
- 44:15
And then finally, we specify as our schema, we want, uh, a, a string, which is the learning itself, the insight we could probably call it, and then, uh, any follow-up questions, which are an array of strings.
- 44:28
And we return that object, which is this type-safe, uh, structured output.
- 44:34
Cool. So we can incorporate that into our main function again. Let's replace all of that, and we can see now that we are... We have our prompt, we have our queries.
- 44:45
We, we, for each query, we're going to search the web, search and process. So we're searching and making sure that the, the result is relevant. And then for each of the results that are returned, we're then gonna pass it to our generateLearnings function, passing in that original query and then the, um, [lips smack] the search result.
- 45:05
So we could, again, test to see how things are looking.
- 45:11
Run the script. Searching the web for requirements to become a D1 shot put athlete. Let's see how many times, uh, I have said that in, um, in this video.
- 45:25
So it's deemed that this is irrelevant, but the next link that it finds is relevant, so it's now processing this search result, and then we're gonna get a learning.
- 45:34
So what do we have here? "To become a D1 shot put athlete, high school athletes typically need to have four years of varsity experience, achieve high state finishes or be state champions, and participate in national events like USATF National Junior Olympic Outdoor Track and Field Championships.
- 45:50
They should also aim for a shot put distance of around," I think that's 60 feet, um, for tier one recruits. Um, and then we've got some follow-up questions. What are the specific training regimens for shot put athletes?
- 46:03
How do Division 1 recruiting standards differ from Division 2 and 3? Some, like, really interesting threads to, to go down. So what we're gonna have to do naturally now is introduce recursion to this whole process, meaning now that we have our learnings, we effectively want to take the follow-up questions, create a new query, and call the entire
- 46:25
process again until, uh, effectively we, we are happy with the, the depth of information that we've gathered. So there are gonna be a few things that we need to do.
- 46:36
One, we need to probably create a new function that can handle this recursion rather than doing it all in the main function.
- 46:44
Two, we're going to have to... We're not gonna be able to track all of our, uh, accumulated research in, in the local function anymore. We're gonna have to create some state or a, a variable outside in, in the global state to be able to track this as we go through, and then we're probably gonna need some types
- 47:02
as well just to help make this all a bit easier. So let's add those now. First things first, we're gonna create a new function, uh, called, uh, deepResearch to, um, to actually...
- 47:16
That will be able to handle our recursion. So all we've done here is taken our previous logic that was within the main function and put it into a new function called deepResearch.
- 47:25
Um, so as you can see, we're, we're generating search queries. We are then searching and process, then we're generating the learning. So same as before, and then in our main function, this time we define our prompt, and then we, um, call that deepResearch function.
- 47:43
So what we're gonna have to do now is actually call this function recursively, but first things first, we're going to need to create, uh, a, a place to store our accumulated research state.
- 47:58
So I'm gonna copy in some code here. Uh, we're defining two types, uh, a learning type, which we've seen before. This was the, the, the learning, the insight from the research, and then follow-up questions.
- 48:09
Um, and then research. This is the core accumulated research object, um, or store. Um, and we store in, like, our original query, the queries that have been performed, um, or the queries, the active queries right now, the search results and accumulation of all of them, uh, learnings, all the insights, and then completed queries.
- 48:31
And at the very end, as this develops, we're going to pass all of that state, that store of information, to one large model that can synthesize all of that and generate our report.
- 48:42
But first, we need to update our deepResearch function to actually update the store as it's iterating through these levels of recursion. So Let's head to our Deep Research function.
- 48:55
I'm gonna copy all of this, and we'll look at what we've updated here. So the first thing that we've done, um, is we've, we've changed the name of one, uh, parameter from query to prompt, just so you know.
- 49:08
Uh, we've also updated our depth and breadth, uh, numbers. So depth, remember, are the levels deep that we go into the, the, the, the strands that will... or the levels of the strands that we'll go down.
- 49:20
And then breadth are those, like, individual forks, how many of them will go down at each level. So we first check to see if a query is undefined. That will be on the first time we're ever running this.
- 49:32
Um, and if it is, we'll set it to the, the, the prompt. We then, like last time, we generate the search queries. This time, we update our queries to be that, that, uh, whatever is returned from the search queries.
- 49:45
Um, and that's a theme as we go through here. We're gonna update our accumulated research to take in our, our search results, our learnings, and our completed queries. And now that we have this all out of the way, we can actually call Deep Research recursively.
- 49:59
Um, and at the same time, we're gonna decrement the depth and breadth so that we can eventually get to a resolution, uh, so this doesn't run forever, and we don't run out of all of our API credits and leave the user waiting for, for way too long.
- 50:14
So let's add that final recursion. I'm going to replace the entire function again, um, just so I don't make any mistakes here. We've added two things here. First, we've said, uh, if the depth is, is zero, uh, just return.
- 50:29
Effectively, like, recursion is done at this point. Uh, return the accumulated research. Um, and now for the new stuff, where before we had the comment saying, "Perform the deep research," now instead we create a new query saying, "The overall research goal is this.
- 50:47
These are the previous search queries that have been performed. Here are some follow-up questions." Um, and then we pass that back and call the entire function again with that new prompt, uh, again, decrementing the depth and the breadth at a more exponential rate.
- 51:08
So I think that's a good a, as good a time as any to, to run the function again and, and kind of see what's happening. We should see it now going into various levels of depth.
- 51:20
Again, I'm saying requirements to become a D1 shot put athlete.
- 51:24
So this, uh, source is deemed irrelevant. It keeps liking to use that one.
- 51:32
Instead, the track recruiting standards is deemed relevant. We're now processing the search result. We're gonna be generating some learnings.
- 51:41
We're now searching the web for what physical and technical skills are essential for a D1 shot put athlete. So we're going a level of depth here.
- 51:49
We found, uh, some, some relevant... Now we're searching again, training and skills needed for D1 shot put athletes,
- 52:00
and so on and so forth. So we can see that it is indeed working.
- 52:09
One thing, though, that we haven't done yet is that we haven't incorporated into... And funny enough, we didn't see this in this example, but the way that our logic works right now, when the model is trying to decide whether a link is relevant, it doesn't have context as to the links that have already been used in other
- 52:28
steps. And naturally, we've been talking about context length, price, speed. We don't, we sh, we surely do not want to provide the same source twice, particularly if it's taking up 10, 20,000 tokens in length.
- 52:43
So what we're gonna do is we're going to go to our searchAndProcess function. We're gonna head into our evaluate tool, and in this evaluate tool, we're also going to pass in previously used search results.
- 52:57
And if the search result exists in that previously used search results, we're gonna say that's an irrelevant source and, and try again.
- 53:07
So I'm going to copy the entire funct-- replace the entire function here. Um, this time we're taking in a new parameter here, accumulatedSources. Um, and if we scroll down, we should see our updated prompt.
- 53:22
We say, "If the page already exists in the existing results, mark it as irrelevant." And we can see we're passing it in in XML tags, existingResults, and stringifying through, passing just the URL, because also we don't need all the content from the page here.
- 53:37
We literally just need the URL. We have one error here, and that's because we added a new argument, but we haven't passed it in here. So I'm going to just pass that in here.
- 53:47
Uh, this will be the accumulatedResearch., I think this is the searchResults, yeah. SearchResults. Perfect. Uh, so if we were to, to run this again, what we would see-- We didn't see it in the previous example before, but if it reused a source, or if Exo returned a source
- 54:12
that it had already used in a previous step, that would now be marked as irrelevant, and that agentic process would continue on in a loop. The last thing that we're gonna wanna do, which I actually don't have in, in this process, is that now that we have all this accumulated research, we wanna give it to a big
- 54:30
model. I, I prefer a, a reasoning model in this case to synthesize all of this information and put it into a report that we can consume to hopefully solve our query.
- 54:41
So let's create a new function. This function is going to be called a very creative name, as per usual, uh, generateReport. It's going to take in that accumulated research, which is typed as such.
- 54:55
Um, and then we're gonna call generateText This time we're using o3-mini rather than our main model. Uh, again, play around with this, clone this, and, and see which models, uh, you like best for this kind of thing.
- 55:07
I found o3-mini very good at this. And then our prompt here, we're saying, "Generate a report based on the following research data." We've got two new lines, and then we stringify the report, returning the final generated text.
- 55:21
So let's actually refactor our main function very quickly to use this new,
- 55:29
uh, this new function. So what we're doing here, uh, we've just inlined our prompt, so we say, uh, what you need to, [chuckles] to be a D1 shot put athlete.
- 55:38
We log out some status updates, and then we pass that research into the generateReport function, await it, and get out a report, which we'll finally write to the file system to a Markdown file.
- 55:51
So I'm going to import fs from fs, and then we're ready to go. So let's see, let's see what this does.
- 56:12
Great. So after about a minute, we've got our report. "Below is an integrated report summarizing the research findings on what it takes to become a Division 1 shot put athlete, along with a rel- with related insights on technique, training, and common beginner pitfalls."
- 56:28
So okay, we've got our, our entire report here. Um, and to be honest, given all of the information that we gave it, it's, that's pretty, it's pretty great. But one thing that we'll note here is that we didn't give the model any guidance on what exactly we wanted this report to look like, and so the model had
- 56:49
to infer. Um, and when you're working with language models, in order to get the best response, you want to leave as little up to the model to infer as possible.
- 57:00
So one thing, I want this to be in a Markdown format. Two, I'd like it to f- uh, to form a bit more of a structure, um, that I want to specify.
- 57:09
And so what we're gonna do here is we're gonna head back to our, uh, generateResearch function, and we're gonna do two things. We're going to... Actually, just one thing, sorry.
- 57:20
We're going to create a system prompt. We're gonna tell it, give it a persona, "You are an expert researcher." We're gonna give it today's date. We're gonna tell it to follow these instructions exactly.
- 57:30
Um, and a few key things in here. We're gonna say, "Use Markdown formatting. Uh, you may use high levels of speculation or prediction, just flag it." Um, and, and in general, we just give it these, these, uh, guidelines that are very research, uh, analyst-oriented.
- 57:49
So I'm gonna run this entire thing again, and we're gonna see the difference in the output.
- 58:12
And here it is. We have our new report. So let's jump into it, and what can we notice right off the bat? We're using Markdown, which is awesome, much more structured.
- 58:22
Uh, we can see that we've got date in line, kind of helpful to have. Um, but, like, the, the level of quality of this kind of report, even just with this basic workflow, really astounds me.
- 58:36
So you can see, "To be considered," like right at the top level, "To be considered a Division 1 shot put prospect, athletes are expected to demonstrate a high level of competitive success, varsity experience, um, typically four years of varsity participation, competitive exposure," uh, we get performance benchmarks, "A benchmark throw of 55 feet.
- 58:56
Elite or top-tier recruits tend to have distances around 60 feet and 18 inches." Like, this is, uh... And the difference between men's and women. It, it is so, so, so cool how, uh, we were able to build this in just 218 lines of code.
- 59:13
Um, so yeah, this is, this is it. This is the session. I hope you enjoyed this. If you have any questions, uh, you can reach me on, um, X, the everything platform, [REDACTED:username].
- 59:28
Uh, feel free to send me a DM. Uh, if you have any questions on building with the SDK, head to sdk.Vercel.ai. Check out our docs. We've got some awesome, uh, guides in the cookbook as well.
- 59:40
Um, and yeah, I hope you enjoyed this. I wanna thank Swyx for asking me to, to do this, uh, session, and I really hope you enjoyed it and hope to see you at the next one.
- 59:50
Take care.