In Chapter 4: Streaming Execution Strategy, we learned how to act like a "Live Sports Commentator," streaming events from the internet in real-time.
At the end of that chapter, we were left with a variable called allContentBlocks. This is a raw, messy pile of data containing everything that happened during the stream: the AI's internal thoughts, the command to search, and the raw JSON coming back from the search provider.
Now, we need to tidy up.
Think of this phase as a Court Stenographer.
allContentBlocks.In this chapter, we will build the logic that turns chaos into order.
allContentBlocks)When the streaming finishes, we have an array of "Blocks." It looks something like this (simplified):
[
{ "type": "text", "text": "I need to check..." },
{ "type": "server_tool_use", "name": "web_search" },
{ "type": "text", "text": "checking google..." },
{ "type": "web_search_tool_result", "content": [{ "url": "..." }] }
]
This is hard to use. The UI can't easily render this, and we can't save this to a database easily. We need to convert this into the clean Output Schema we defined in Chapter 2: Data Contract (Schemas).
We use a helper function called makeOutputFromSearchResponse. Its job is to filter through the noise and extract the gold.
First, we prepare empty arrays to hold our clean data.
function makeOutputFromSearchResponse(blocks, query, duration) {
const results = [] // We will put clean hits here
let textAcc = '' // We will accumulate text here
// ... loop logic starts here
Explanation: We are preparing to iterate through the list of blocks. We separate "Text" (commentary) from "Results" (actual links).
We loop through every block. If we find a web_search_tool_result, we grab the content.
for (const block of blocks) {
// Did we find a search result block?
if (block.type === 'web_search_tool_result') {
// Extract just the Title and URL
const hits = block.content.map(r => ({
title: r.title,
url: r.url
}))
// Save it to our clean list
results.push({ content: hits })
}
// ... handle text blocks ...
}
Explanation:
The raw block might contain extra metadata we don't need. Here, we map it strictly to { title, url }. This ensures our output matches our Schema perfectly.
Finally, we wrap it all up in the Output object.
return {
query: query, // "Apple Stock"
results: results, // [{ title: "...", url: "..." }]
durationSeconds: duration // 1.2s
}
} // End of function
Explanation: This return value is the Official Record. It is clean, typed, and ready to be sent to the User Interface (which we will see in Chapter 6).
We have processed the results for our internal app state. However, the AI (Claude) is still waiting for an answer!
The AI doesn't read our internal database objects. It reads Text. We must take our clean object and convert it ("serialize" it) into a text string that the AI can understand.
We do this in a method called mapToolResultToToolResultBlockParam.
We loop through our clean results and build a long string.
mapToolResultToToolResultBlockParam(output, toolUseID) {
const { query, results } = output
// Start the message
let formattedOutput = `Web search results for query: "${query}"\n\n`
// Loop through results and turn them into text
results.forEach(result => {
if (result.content?.length > 0) {
// Turn the JSON object into a string representation
formattedOutput += `Links: ${JSON.stringify(result.content)}\n\n`
}
})
// ... continued below
Explanation:
We use JSON.stringify to turn the array of links into a text representation. The AI is smart enough to read JSON text and understand it.
This is a "Prompt Engineering" trick. Even though we told the AI to cite sources in Chapter 3: Prompt Engineering Context, LLMs can sometimes forget rules during long conversations.
We inject a Reminder at the very end of the data package.
// Append a strict rule to the end of the data
formattedOutput +=
'\nREMINDER: You MUST include the sources above in your response.'
return {
tool_use_id: toolUseID,
type: 'tool_result',
content: formattedOutput.trim(),
}
},
Explanation: This is the last thing the AI "hears" before it generates its answer. By placing the reminder here, we significantly increase the chance that the AI will correctly link to the websites it found.
Here is the complete journey of the data, from the raw stream to the final message sent to the AI.
You might wonder: "Why not just send the raw JSON straight to the AI?"
Output object is small.We have now completed the backend logic for the WebSearchTool.
The tool works! The AI can search, find data, and answer the question.
However, the Human User is still looking at a blank screen. They don't see the internal JSON objects. They need a beautiful, interactive display to see what the AI found.
In the final chapter, we will learn how to render these results in the User Interface.
Next Chapter: Interface Rendering (UI)
Generated by Code IQ