<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: James Collins</title>
    <description>The latest articles on DEV Community by James Collins (@james_collins).</description>
    <link>https://dev.to/james_collins</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3434874%2F9b433a5f-f545-44cd-a065-cd766558b101.png</url>
      <title>DEV Community: James Collins</title>
      <link>https://dev.to/james_collins</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/james_collins"/>
    <language>en</language>
    <item>
      <title>Build an AI Research Agent for Slack and Linear with SerpApi</title>
      <dc:creator>James Collins</dc:creator>
      <pubDate>Tue, 24 Mar 2026 17:48:29 +0000</pubDate>
      <link>https://dev.to/james_collins/build-an-ai-research-agent-for-slack-and-linear-with-serpapi-38cm</link>
      <guid>https://dev.to/james_collins/build-an-ai-research-agent-for-slack-and-linear-with-serpapi-38cm</guid>
      <description>&lt;p&gt;Context switching is the silent killer of developer productivity. You are deep in a Linear ticket or debugging a Slack thread, you hit a technical question, and suddenly you are in a browser tab reading through Stack Overflow and pasting links back to your team. Multiply that by every developer on your team, several times a day, and you start to see how much focused time disappears.&lt;/p&gt;

&lt;p&gt;What if the tools you already use could do that research for you?&lt;/p&gt;

&lt;p&gt;In this tutorial, we build a cross-platform *&lt;strong&gt;&lt;em&gt;Research Agent&lt;/em&gt;&lt;/strong&gt;* — a single Python backend that plugs into both Slack and Linear. You can mention it in a channel or assign it to a ticket, and it will search the web using SerpApi, synthesize the results with an LLM, and post a cited answer directly in your workspace. No tab switching required. We will mostly focus on Linear example since that's where the assignment happens explicitly, but Slack extension is supported as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://dev.to/james_collins/build-an-ai-research-agent-for-slack-and-linear-with-serpapi-38cm#why-build-a-workspace-research-agent"&gt;Why Build a Workspace Research Agent&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://dev.to/james_collins/build-an-ai-research-agent-for-slack-and-linear-with-serpapi-38cm#architecture-one-backend-two-platforms"&gt;Architecture: One Backend, Two Platforms&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://dev.to/james_collins/build-an-ai-research-agent-for-slack-and-linear-with-serpapi-38cm#the-research-engine-serpapi--llm"&gt;The Research Engine: SerpApi + LLM&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://dev.to/james_collins/build-an-ai-research-agent-for-slack-and-linear-with-serpapi-38cm#connecting-to-slack"&gt;Connecting to Slack&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://dev.to/james_collins/build-an-ai-research-agent-for-slack-and-linear-with-serpapi-38cm#connecting-to-linears-agent-api"&gt;Connecting to Linear’s Agent API&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://dev.to/james_collins/build-an-ai-research-agent-for-slack-and-linear-with-serpapi-38cm#running-it-locally"&gt;Running It Locally&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://dev.to/james_collins/build-an-ai-research-agent-for-slack-and-linear-with-serpapi-38cm#seeing-it-in-action"&gt;Seeing It in Action&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://dev.to/james_collins/build-an-ai-research-agent-for-slack-and-linear-with-serpapi-38cm#conclusion"&gt;Conclusion&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Build a Workspace Research Agent
&lt;/h2&gt;

&lt;p&gt;Project management tools and chat apps are excellent at tracking internal context — who owns a task, what the status is, what decisions were made. But they have a blind spot when it comes to external information: updated documentation, recent CVEs, syntax examples, framework comparisons, or trending approaches to a problem.&lt;/p&gt;

&lt;p&gt;Developers bridge that gap manually, dozens of times a day, mostly submitting prompts to the LLM and then coming back and hour later to pick up on the necessary context. There is no task -&amp;gt; in progress -&amp;gt; done update loop here.&lt;/p&gt;

&lt;p&gt;An AI research agent solves this by bringing live web data into the conversation. Instead of leaving Slack or Linear, a developer tags the agent with a question. The agent fetches structured search results via SerpApi, passes them to an LLM for synthesis, and posts a concise, cited answer right where the question was asked.&lt;/p&gt;

&lt;p&gt;The key insight is that SerpApi returns clean, structured JSON — titles, snippets, links — rather than raw HTML. Our agent never scrapes or parses web pages. It reasons over high-quality, normalized data from Google, Google Scholar, YouTube, and dozens of other engines. The output is a complete research with references and an extendable engine to back it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: One Backend, Two Platforms
&lt;/h2&gt;

&lt;p&gt;Building a cross-platform bot means dealing with different platform lifecycles. Slack requires you to acknowledge an event within 3 seconds. Linear’s Agent API requires your agent to emit a “thought” activity within 10 seconds of receiving an assignment. If you miss either deadline and the platform marks your bot as unresponsive.&lt;/p&gt;

&lt;p&gt;To handle this cleanly, we built a single FastAPI server with two webhook endpoints — &lt;code&gt;/slack/events&lt;/code&gt; and &lt;code&gt;/linear/webhooks&lt;/code&gt; — and a shared research engine behind them. Both endpoints immediately satisfy their platform’s timeout constraint and then hand the actual work to a background task.&lt;/p&gt;

&lt;p&gt;Here is what the flow looks like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; *&lt;strong&gt;&lt;em&gt;Ingest&lt;/em&gt;&lt;/strong&gt;*: A webhook arrives from Slack (app mention) or Linear (agent session event).&lt;/li&gt;
&lt;li&gt; *&lt;strong&gt;&lt;em&gt;Acknowledge&lt;/em&gt;&lt;/strong&gt;*: The endpoint responds instantly — Slack Bolt calls &lt;code&gt;ack()&lt;/code&gt; automatically; the Linear handler returns HTTP 200 and spawns a background coroutine.&lt;/li&gt;
&lt;li&gt; *&lt;strong&gt;&lt;em&gt;Search&lt;/em&gt;&lt;/strong&gt;*: The research engine calls SerpApi with the user’s query and gets structured JSON results.&lt;/li&gt;
&lt;li&gt; *&lt;strong&gt;&lt;em&gt;Synthesize&lt;/em&gt;&lt;/strong&gt;*: An LLM reads the search results and produces a markdown summary with source citations.&lt;/li&gt;
&lt;li&gt; *&lt;strong&gt;&lt;em&gt;Respond&lt;/em&gt;&lt;/strong&gt;*: The answer is posted back — as a threaded Slack message or a Linear agent activity.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The entire backend is async Python. FastAPI handles routing, Slack Bolt’s &lt;code&gt;AsyncApp&lt;/code&gt; handles Slack-specific event parsing, and a lightweight &lt;code&gt;httpx&lt;/code&gt;-based client handles Linear’s GraphQL API. Everything shares the same &lt;code&gt;perform_research()&lt;/code&gt; function, platform-specific code only exists at the edges.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Research Engine: SerpApi + LLM
&lt;/h2&gt;

&lt;p&gt;The core of the agent is surprisingly simple. Two functions do all the work: one fetches search results, the other synthesizes them.&lt;/p&gt;

&lt;p&gt;For search, we use SerpApi’s Python client. The agent automatically picks the right engine based on the query. For this article, we keep the search loop simple, but you can read about more advanced approaches on the &lt;a href="https://serpapi.com/blog/building-a-fast-self-hosted-research-agent-with-openai-models-serpapi/" rel="noopener noreferrer"&gt;prior blog posts&lt;/a&gt;. If someone asks about a “research paper,” it routes to Google Scholar; if they want a “video tutorial,” it routes to YouTube; otherwise it defaults to Google Search:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serpapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serpapi_api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;_pick_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because SerpApi returns normalized JSON regardless of the engine, the rest of the pipeline does not need to care which search backend was used. We extract &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;snippet&lt;/code&gt;, and &lt;code&gt;link&lt;/code&gt; from each result and format them into a context block.&lt;/p&gt;

&lt;p&gt;For synthesis, we pass those results to an LLM with a system prompt that enforces citation discipline, summarize the findings, always cite sources using the provided URLs, never fabricate beyond what the search results contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;openai_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RESEARCH_SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer this query: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Search results:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The low temperature keeps the output factual. SerpApi handles proxy rotation, CAPTCHA solving, and result parsing behind the scenes, so our code never touches raw HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting to Slack
&lt;/h2&gt;

&lt;p&gt;For Slack, we use the &lt;a href="https://slack.dev/bolt-python/" rel="noopener noreferrer"&gt;Slack Bolt&lt;/a&gt; framework. Bolt handles event parsing, signature verification, and the 3-second acknowledgment automatically — we just write a handler for the &lt;code&gt;app_mention&lt;/code&gt; event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app_mention&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt;  &lt;span class="k"&gt;def&lt;/span&gt;  &lt;span class="nf"&gt;handle_mention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;say&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;thread_ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thread_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt;  &lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Researching...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;thread_ts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;thread_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="k"&gt;await&lt;/span&gt;  &lt;span class="nf"&gt;perform_research&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_extract_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt;  &lt;span class="nf"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_slack_format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="n"&gt;thread_ts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;thread_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When someone types &lt;code&gt;@ResearchBot What is the best way to handle database migrations in Django?&lt;/code&gt;, the handler strips out the bot mention to get the raw question, posts a “Researching…” message so the user knows something is happening, runs the SerpApi + LLM pipeline, and replies in the same thread with the cited answer.&lt;/p&gt;

&lt;p&gt;One detail worth noting: Slack uses its own link format (&lt;code&gt;&amp;lt;url|text&amp;gt;&lt;/code&gt;) instead of standard markdown. Since our research engine always outputs standard markdown, we convert links at the boundary with a simple regex. The engine itself stays platform-agnostic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting to Linear’s Agent API
&lt;/h2&gt;

&lt;p&gt;Linear’s &lt;a href="https://linear.app/developers/agents" rel="noopener noreferrer"&gt;Agent API&lt;/a&gt; is the more interesting integration. Unlike a basic webhook that creates comments, Linear’s agent system gives your bot a first-class identity in the workspace. It shows up in the assignee dropdown, it can be @mentioned in issues and documents, and its responses appear as structured *&lt;strong&gt;&lt;em&gt;agent activities&lt;/em&gt;&lt;/strong&gt;* with visible thinking states, not just plain comments.&lt;/p&gt;

&lt;p&gt;When a user assigns your agent to an issue or mentions it in a comment, Linear creates an &lt;code&gt;AgentSession&lt;/code&gt; and sends a webhook to your server. Your agent communicates back through typed activities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  *&lt;strong&gt;&lt;em&gt;thought&lt;/em&gt;&lt;/strong&gt;*: Shows a thinking indicator (e.g., “Performing web research via SerpApi…”)&lt;/li&gt;
&lt;li&gt;  *&lt;strong&gt;&lt;em&gt;response&lt;/em&gt;&lt;/strong&gt;*: The final answer, rendered as rich markdown in the issue&lt;/li&gt;
&lt;li&gt;  *&lt;strong&gt;&lt;em&gt;error&lt;/em&gt;&lt;/strong&gt;*: Displayed with error styling if something goes wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The webhook handler needs to respect two constraints: return HTTP 200 within 5 seconds, and emit a &lt;code&gt;thought&lt;/code&gt; within 10 seconds. We handle this by returning immediately and spawning the research work as a background coroutine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AgentSessionEvent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_handle_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;  &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The background task sends the &lt;code&gt;thought&lt;/code&gt; activity right away — well within the 10-second window — then runs the research pipeline and posts the result as a &lt;code&gt;response&lt;/code&gt;. The query is extracted from Linear’s &lt;code&gt;promptContext&lt;/code&gt; field, which contains the issue title, description, and any relevant context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;linear_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_thought&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Performing web research via SerpApi...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;perform_research&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;linear_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For GraphQL communication, we wrote a thin async client using &lt;code&gt;httpx&lt;/code&gt; rather than pulling in a full GraphQL library. All we need are two mutations — &lt;code&gt;agentActivityCreate&lt;/code&gt; for posting activities and &lt;code&gt;agentSessionUpdate&lt;/code&gt; for managing session state — so raw query strings are more than sufficient.&lt;/p&gt;

&lt;p&gt;Linear also signs every webhook with HMAC-SHA256, which we verify against the raw request body before processing anything. And if a user sends a follow-up message within an existing session, Linear dispatches a &lt;code&gt;prompted&lt;/code&gt; event (instead of &lt;code&gt;created&lt;/code&gt;), making the interaction conversational, the agent picks up the new message and researches again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running It Locally
&lt;/h2&gt;

&lt;p&gt;Both Slack and Linear support private development without any app directory submission or review process. You can have the full end-to-end flow running in minutes.&lt;/p&gt;

&lt;p&gt;The project uses &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt; for dependency management and Python 3.12+:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git  clone  https://github.com/serpapi/serpapi-research-bot.git
&lt;span class="nb"&gt;cd  &lt;/span&gt;serpapi-research-agent
uv  &lt;span class="nb"&gt;sync
cp&lt;/span&gt;  .env.example  .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fill in your &lt;code&gt;.env&lt;/code&gt; with API keys for &lt;a href="https://serpapi.com/manage-api-key" rel="noopener noreferrer"&gt;SerpApi&lt;/a&gt;, &lt;a href="https://platform.openai.com/api-keys" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;, and the Slack/Linear credentials from your dev app configurations. Then start the server and expose it via ngrok:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv  run  uvicorn  serpapi_research_agent.main:app  &lt;span class="nt"&gt;--reload&lt;/span&gt;  &lt;span class="nt"&gt;--port&lt;/span&gt;  3000

&lt;span class="c"&gt;# In another terminal:&lt;/span&gt;
ngrok  http  3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point your Slack Event Subscriptions Request URL and Linear app webhook URL to the ngrok tunnel, and you are ready to test. For Slack, invite the bot to a channel and mention it. For Linear, create an issue and assign the agent as a delegate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing It in Action
&lt;/h2&gt;

&lt;p&gt;We will focus on Linear example, since it is more interesting and has a better Agent integration, but setting up Slack is straightforward as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Linear
&lt;/h3&gt;

&lt;p&gt;For Linear, first we will need to create a new Linear Oauth app. Sample settings below, you get the URL from ngrok, add /linear/webhooks to it, and add it to the webhook section. You will also need to get a Linear ouath access token, you can take it be submitting a request with your client ID, client secret, and grant_type=client_credentials (note that client credentials must be enabled during the auth) setup. Then save all the received credentials into the .env file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5037pvuao5toy5e3ukq5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5037pvuao5toy5e3ukq5.png" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the agent is properly configured and everything is running, you can open a ticket and assign it an issue. The Research Bot will appear as a first-class Linear member:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpb7xhljejt7dhhkhln3w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpb7xhljejt7dhhkhln3w.png" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And once the research is actually complete, then the agent will follow with a &lt;code&gt;response&lt;/code&gt; activity and a full response content.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsko13m21mfla2ghy4lkw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsko13m21mfla2ghy4lkw.png" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By integrating SerpApi and an LLM directly into Slack and Linear, we built a research assistant that meets developers exactly where they work. The architecture is deliberately simple — a single FastAPI server with a shared research engine and thin platform-specific adapters at the edges.&lt;/p&gt;

&lt;p&gt;SerpApi handles the hard parts of web search: proxy rotation, CAPTCHA solving, and result normalization across multiple engines. The LLM handles synthesis and citation. And the platform integrations handle the last mile — getting the answer back into the right thread or ticket, formatted correctly, within each platform’s timeout constraints.&lt;/p&gt;

&lt;p&gt;The full source code is available on &lt;a href="https://github.com/serpapi/serpapi-research-bot" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. You can have it running against your own Slack workspace and Linear instance in under 15 minutes.&lt;/p&gt;

&lt;p&gt;Ready to build your own workspace agent? &lt;a href="https://serpapi.com/" rel="noopener noreferrer"&gt;Create a free SerpApi account&lt;/a&gt; to get started.&lt;a href="https://dev.tourl"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>programming</category>
    </item>
    <item>
      <title>Competitive Intelligence Agent: From Slides to Live Signals</title>
      <dc:creator>James Collins</dc:creator>
      <pubDate>Mon, 02 Mar 2026 14:13:14 +0000</pubDate>
      <link>https://dev.to/james_collins/competitive-intelligence-agent-from-slides-to-live-signals-4bn3</link>
      <guid>https://dev.to/james_collins/competitive-intelligence-agent-from-slides-to-live-signals-4bn3</guid>
      <description>&lt;p&gt;In many B2B organizations, "competitive intelligence" still means hunting through old slide decks, ad‑hoc notes, and a long history of Slack threads. Even if you track competitors somewhere in your CRM, the reality on the ground changes faster than those assets: pricing pages move, job descriptions hint at new bets, and funding announcements reset roadmaps overnight. When your analysis or enablement material lags behind, it becomes background noise instead of a strategic guide.&lt;/p&gt;

&lt;p&gt;Historically, closing this gap meant a lot of copy‑paste work: open a few tabs, search for news, skim job boards, and then try to reconcile that with whatever your CRM says about the account. With modern tooling, this workflow can be turned into a repeatable pattern. By combining &lt;strong&gt;OpenAI&lt;/strong&gt;, optional &lt;strong&gt;HubSpot CRM&lt;/strong&gt; access, and &lt;a href="https://serpapi.com/" rel="noopener noreferrer"&gt;SerpApi&lt;/a&gt;, a Competitive Intelligence Agent can pull live web signals, overlay your internal context, and return a sourced briefing on demand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge: Stale Assets and Fragmented Context
&lt;/h2&gt;

&lt;p&gt;Competitive landscapes do not stand still, but the artifacts we use to describe them often do. Common issues include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;Static collateral in a moving market:&lt;/strong&gt; Competitive decks are produced for a launch, a QBR, or a training session, then slowly drift away from reality.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Many sources, no single view:&lt;/strong&gt; Product marketers and PMs check pricing pages, comparison sites, review platforms, press releases, and job listings—but there is no unified answer to "What is happening with this competitor right now?"&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Internal knowledge is siloed:&lt;/strong&gt; Your CRM may already show where a competitor appears in deals and which stakeholders you know, yet that insight rarely shows up in market analysis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bringing all of this into a single, timely briefing used to require custom glue code. Standards like the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; aim to give models a consistent way to talk to APIs and databases, but in this project we use a straightforward approach: direct integrations with SerpApi and, optionally, the HubSpot SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Competitive Intelligence Agent Behaves
&lt;/h2&gt;

&lt;p&gt;The agent follows a simple loop—&lt;strong&gt;Plan → Gather → Summarize&lt;/strong&gt;—to turn scattered data into a coherent briefing.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Plan
&lt;/h3&gt;

&lt;p&gt;Given a request such as &lt;em&gt;"Give me a competitive snapshot of acme.com, including what we know in HubSpot,"&lt;/em&gt; the model decides which tools to use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;External:&lt;/strong&gt; SerpApi‑backed web, news, and jobs search to understand positioning, recent milestones, and hiring focus.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Internal (optional):&lt;/strong&gt; HubSpot company, contact, and activity lookups to see how this competitor (or customer with a known competitor) appears in your own pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Gather
&lt;/h3&gt;

&lt;p&gt;The agent then calls the tools it selected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;SerpApi:&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Web search for pricing pages, comparison articles, and review sites&lt;/li&gt;
&lt;li&gt;News search for launches, funding, and partnerships within a defined time window&lt;/li&gt;
&lt;li&gt;Jobs search for roles and locations that hint at strategic priorities&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;HubSpot SDK (optional):&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Find a company by domain if it already exists in your CRM&lt;/li&gt;
&lt;li&gt;Look up specific contacts by email&lt;/li&gt;
&lt;li&gt;Retrieve a timeline of notes, calls, and emails associated with those contacts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each tool returns structured JSON that can be reasoned about and cited.  &lt;/p&gt;

&lt;h3&gt;
  
  
  3. Summarize
&lt;/h3&gt;

&lt;p&gt;Once enough information has been collected, the model stops calling tools and produces a concise briefing. A typical output might look like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Acme emphasizes low‑latency analytics for operational workloads [1], [2]. Over the past month they announced an EU expansion and a partnership with a major cloud provider [3]. In our HubSpot data, we see one open deal where Acme is listed as a competing vendor, plus a Director‑level contact who raised concerns about migration cost three months ago [4]. For upcoming conversations, we should highlight data residency guarantees and total cost of ownership versus their current stack."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The result is something closer to a one‑page memo than a raw list of links.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Building Blocks
&lt;/h2&gt;

&lt;p&gt;The repository contains a complete implementation. At a high level, the Competitive Intelligence Agent is assembled from three layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. SerpApi for Web, News, and Jobs
&lt;/h3&gt;

&lt;p&gt;SerpApi handles live search across several Google surfaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;Web search:&lt;/strong&gt; Company sites, competitor comparison pages, and review content.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;News search:&lt;/strong&gt; Time‑bounded news about funding, launches, acquisitions, or leadership changes.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Jobs search:&lt;/strong&gt; Open roles and locations that reveal where and how a company is investing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because SerpApi returns normalized JSON instead of HTML, the agent can focus on reasoning over titles, snippets, links, and timestamps rather than scraping and parsing.\n+&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Optional HubSpot SDK for Internal Context
&lt;/h3&gt;

&lt;p&gt;If you provide a &lt;code&gt;HUBSPOT_ACCESS_TOKEN&lt;/code&gt;, the agent also exposes HubSpot‑backed tools that answer "what do we already know?":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; &lt;code&gt;hubspot_search_company_by_domain&lt;/code&gt; — discover whether the company is already in your CRM and retrieve its basic record.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;hubspot_get_contact_by_email&lt;/code&gt; — fetch contact details for known stakeholders.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;hubspot_get_contact_activity_history&lt;/code&gt; — pull a summarized history of notes, calls, and emails tied to that contact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This turns the agent from a pure external‑research helper into a bridge between the market and your existing pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. OpenAI for Orchestration and Writing
&lt;/h3&gt;

&lt;p&gt;An OpenAI model coordinates the process by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Selecting which tools to invoke based on the question.&lt;/li&gt;
&lt;li&gt;Interpreting the structured JSON returned by SerpApi and HubSpot.&lt;/li&gt;
&lt;li&gt;Drafting a narrative answer with clear sections and numbered citations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Python layer is responsible for defining tools, handling authentication, and streaming tool results back into the conversation; the model is responsible for the reasoning and writing.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Practices for Reliable Briefings
&lt;/h2&gt;

&lt;p&gt;To keep outputs trustworthy and useful, the implementation follows a few simple practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;Explicit citations:&lt;/strong&gt; External claims—such as funding rounds, launch dates, or pricing changes—are tied to numbered references so readers can verify them.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Time‑aware queries:&lt;/strong&gt; Loose phrases like "recent" or "last month" are translated into date filters for SerpApi, ensuring news queries match the intended window.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Read‑only CRM access:&lt;/strong&gt; HubSpot integration in this project is observational. The agent reads companies, contacts, and activities but does not create or update records.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These constraints make it easier to embed the agent into real workflows without surprising side effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where MCP Fits In
&lt;/h2&gt;

&lt;p&gt;If you are exploring the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;, this project offers a concrete pattern you could later port into that world. MCP aims to give language models a unified way to talk to tools and data sources. In contrast, this agent wires SerpApi and HubSpot directly via their SDKs and APIs.&lt;/p&gt;

&lt;p&gt;Functionally, the shape is similar: the model chooses tools, the runtime executes them, and the model synthesizes an answer. A future variant could swap the direct integrations for &lt;strong&gt;SerpApi MCP&lt;/strong&gt; and a &lt;strong&gt;HubSpot MCP&lt;/strong&gt; server while preserving the same Plan → Gather → Summarize behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Continuous Competitive Sensing
&lt;/h2&gt;

&lt;p&gt;Instead of occasionally updating a competitive deck, teams can move toward &lt;strong&gt;continuous competitive sensing&lt;/strong&gt;—asking for a current snapshot of a market or rival whenever they need it.\n+&lt;/p&gt;

&lt;p&gt;By pairing SerpApi, OpenAI, and optional HubSpot CRM, the Competitive Intelligence Agent turns scattered signals into a single, sourced briefing. Strategy, product, and revenue teams get fast context without tab‑sprawl, and they can make decisions with both public information and their own CRM history in view.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>hubspot</category>
      <category>serpapi</category>
    </item>
    <item>
      <title>How to use SerpApi engine schemas in SerpApi MCP to improve tool call quality</title>
      <dc:creator>James Collins</dc:creator>
      <pubDate>Tue, 03 Feb 2026 13:16:43 +0000</pubDate>
      <link>https://dev.to/james_collins/how-to-use-serpapi-engine-schemas-in-serpapi-mcp-to-improve-tool-call-quality-5ek6</link>
      <guid>https://dev.to/james_collins/how-to-use-serpapi-engine-schemas-in-serpapi-mcp-to-improve-tool-call-quality-5ek6</guid>
      <description>&lt;p&gt;The Model Context Protocol (MCP) makes it possible for AI assistants to call external tools in a structured, standardized way. If you’re new to MCP, we recommend starting with our overview: &lt;a href="https://serpapi.com/blog/model-context-protocol-mcp-a-unified-standard-for-ai-agents-and-tools/" rel="noopener noreferrer"&gt;Model Context Protocol (MCP): A Unified Standard for AI Agents and Tools&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;SerpApi’s MCP server already exposes a unified &lt;code&gt;search&lt;/code&gt; tool that can query dozens of SerpApi engines - Google Search, News, Flights, Shopping, and more. Now, we’ve introduced a major upgrade: &lt;strong&gt;engine schemas are exposed as MCP resources&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This means that for every SerpApi engine, you can now programmatically discover exactly which parameters it accepts, their types, and how they’re meant to be used - directly from MCP. This unlocks a new level of accuracy, reliability, and automation for AI-powered developer workflows.&lt;/p&gt;

&lt;p&gt;In this post, we’ll explain how engine schemas work, why they matter, and how to use them in practice - including a hands-on example using Google Flights from VS Code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick recap: SerpApi MCP in your workflow
&lt;/h2&gt;

&lt;p&gt;SerpApi MCP lets you connect AI tools (like Claude Desktop, Cursor, VS Code MCP-compatible extensions or your custom AI agents) directly to SerpApi’s live search infrastructure.&lt;/p&gt;

&lt;p&gt;Once configured, your AI assistant can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Discover available search engines&lt;/li&gt;
&lt;li&gt;Call the &lt;code&gt;search&lt;/code&gt; tool with structured parameters&lt;/li&gt;
&lt;li&gt;Receive normalized JSON results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you haven’t set it up yet, you’ll need a SerpApi API key. You can obtain one at &lt;a href="https://serpapi.com" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then add SerpApi MCP to your MCP configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"serpapi"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.serpapi.com/YOUR_SERPAPI_API_KEY/mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once connected, your AI assistant can immediately start interacting with SerpApi tools and resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s new: Engine schemas as MCP resources
&lt;/h2&gt;

&lt;p&gt;Previously, the LLM had to guess which parameters each SerpApi engine accepted or rely on hardcoded knowledge.&lt;/p&gt;

&lt;p&gt;Now, each engine exposes its own schema as an MCP resource.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this means
&lt;/h3&gt;

&lt;p&gt;With the new feature, MCP exposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A list of available engines&lt;/li&gt;
&lt;li&gt;A detailed parameter schema for each engine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conceptually, this looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;serpapi://engines&lt;/code&gt; → lists all supported engines&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;serpapi://engines/google_flights&lt;/code&gt; → returns the schema for Google Flights&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;serpapi://engines/google_shopping&lt;/code&gt; → returns the schema for Google Shopping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each schema describes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parameter names&lt;/li&gt;
&lt;li&gt;Data types&lt;/li&gt;
&lt;li&gt;Optional vs required fields&lt;/li&gt;
&lt;li&gt;Parameter descriptions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives AI agents a real-time contract for every engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  How engine schemas improve tool call quality
&lt;/h2&gt;

&lt;p&gt;This upgrade solves several real-world problems when building AI-powered integrations.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. No more guessing parameter names
&lt;/h3&gt;

&lt;p&gt;Instead of inventing fields like &lt;code&gt;origin&lt;/code&gt; or &lt;code&gt;destination&lt;/code&gt;, the agent can see the real API fields such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;departure_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arrival_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;departure_date&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Engine-specific optimization
&lt;/h3&gt;

&lt;p&gt;Each SerpApi engine has its own unique features. Schemas allow the assistant to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use flight-specific filters for Google Flights&lt;/li&gt;
&lt;li&gt;Use price ranges for Google Shopping&lt;/li&gt;
&lt;li&gt;Use topic tokens for Google News&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Fewer failed requests
&lt;/h3&gt;

&lt;p&gt;When parameters are generated directly from the schema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Invalid arguments are avoided&lt;/li&gt;
&lt;li&gt;Missing required fields are detected earlier&lt;/li&gt;
&lt;li&gt;Requests become deterministic and repeatable&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Better developer experience
&lt;/h3&gt;

&lt;p&gt;In IDEs like VS Code, schemas unlock:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better prompt grounding&lt;/li&gt;
&lt;li&gt;Smarter auto-completion&lt;/li&gt;
&lt;li&gt;Self-documenting integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example: Using Google Flights with MCP engine schemas
&lt;/h2&gt;

&lt;p&gt;Let’s walk through a real example.&lt;/p&gt;

&lt;p&gt;Imagine you want your AI assistant to search for round-trip flights from San Francisco (SFO) to New York (JFK).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Discover available engines
&lt;/h3&gt;

&lt;p&gt;The assistant can query the engine index resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;serpapi://engines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns a list including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;google_search&lt;/li&gt;
&lt;li&gt;google_news&lt;/li&gt;
&lt;li&gt;google_shopping&lt;/li&gt;
&lt;li&gt;google_flights&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent selects &lt;code&gt;google_flights&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Load the Google Flights schema
&lt;/h3&gt;

&lt;p&gt;Next, the assistant fetches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;serpapi://engines/google_flights
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns a schema describing supported parameters such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;departure_id&lt;/li&gt;
&lt;li&gt;arrival_id&lt;/li&gt;
&lt;li&gt;departure_date&lt;/li&gt;
&lt;li&gt;return_date&lt;/li&gt;
&lt;li&gt;currency&lt;/li&gt;
&lt;li&gt;travel_class&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the assistant knows exactly what Google Flights accepts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Make a structured search call
&lt;/h3&gt;

&lt;p&gt;Using the schema, the AI can generate a correct MCP tool call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"engine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"google_flights"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"departure_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SFO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"arrival_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JFK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"departure_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"return_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-20"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the parameters match the schema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The request is valid&lt;/li&gt;
&lt;li&gt;No field guessing is involved&lt;/li&gt;
&lt;li&gt;Results are returned immediately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The response contains structured flight data including prices, airlines, durations, and layovers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: Shopping search with Google Shopping
&lt;/h2&gt;

&lt;p&gt;Schemas are just as powerful for product search.&lt;/p&gt;

&lt;p&gt;Let’s say you want to find wireless earbuds under $100.&lt;/p&gt;

&lt;h3&gt;
  
  
  Load the engine schema
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;serpapi://engines/google_shopping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll discover parameters such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;q&lt;/li&gt;
&lt;li&gt;min_price&lt;/li&gt;
&lt;li&gt;max_price&lt;/li&gt;
&lt;li&gt;gl&lt;/li&gt;
&lt;li&gt;hl&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Perform the search
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"engine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"google_shopping"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"q"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wireless earbuds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"min_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"max_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"gl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schema ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The right filter fields are used&lt;/li&gt;
&lt;li&gt;The query structure matches the engine&lt;/li&gt;
&lt;li&gt;The response is clean and predictable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Using schemas inside VS Code
&lt;/h2&gt;

&lt;p&gt;VS Code is one of the most popular environments for MCP-powered workflows.&lt;/p&gt;

&lt;p&gt;With SerpApi MCP configured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your AI assistant can dynamically load engine schemas&lt;/li&gt;
&lt;li&gt;Generate valid tool calls&lt;/li&gt;
&lt;li&gt;Iterate on search logic without switching tabs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This enables workflows like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Find the cheapest flights under $500"&lt;/li&gt;
&lt;li&gt;"Track shopping price drops"&lt;/li&gt;
&lt;li&gt;"Monitor breaking news by topic"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All powered by real-time schema-driven requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary: Why this feature matters
&lt;/h2&gt;

&lt;p&gt;By exposing engine schemas as MCP resources, SerpApi MCP becomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More reliable&lt;/li&gt;
&lt;li&gt;More developer-friendly&lt;/li&gt;
&lt;li&gt;More automation-ready&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accurate parameter generation&lt;/li&gt;
&lt;li&gt;Engine-aware AI behavior&lt;/li&gt;
&lt;li&gt;Lower error rates&lt;/li&gt;
&lt;li&gt;Faster integration cycles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This turns SerpApi MCP into a self-describing API layer for AI agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get started today
&lt;/h2&gt;

&lt;p&gt;To try engine schemas with SerpApi MCP:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a SerpApi account: &lt;a href="https://serpapi.com" rel="noopener noreferrer"&gt;https://serpapi.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Enable MCP using your API key&lt;/li&gt;
&lt;li&gt;Connect from VS Code, Claude Desktop, or Cursor&lt;/li&gt;
&lt;li&gt;Start exploring &lt;code&gt;serpapi://engines&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you’re building AI-powered tools that rely on search, shopping data, travel data, or news intelligence - engine schemas will dramatically improve the quality and reliability of your integrations.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>serpapi</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Sales Assistant with real-time market awareness and CRM insights</title>
      <dc:creator>James Collins</dc:creator>
      <pubDate>Wed, 21 Jan 2026 10:50:40 +0000</pubDate>
      <link>https://dev.to/james_collins/building-a-sales-assistant-with-real-time-market-awareness-and-crm-insights-3dj6</link>
      <guid>https://dev.to/james_collins/building-a-sales-assistant-with-real-time-market-awareness-and-crm-insights-3dj6</guid>
      <description>&lt;p&gt;In modern B2B sales, the &lt;strong&gt;“Context Gap”&lt;/strong&gt; can limit deal effectiveness. Even with comprehensive CRM records—emails, call notes, and deal stages—sales teams often miss critical real-time developments in their prospects’ businesses. If a lead recently raised funding or launched a new product and outreach does not reflect it, it risks being overlooked in a crowded inbox.&lt;/p&gt;

&lt;p&gt;Traditionally, closing this gap required manual research: searching online, reviewing news, and checking CRM records. Today, this process can be automated. By integrating &lt;strong&gt;OpenAI&lt;/strong&gt;, the &lt;strong&gt;HubSpot CRM SDK&lt;/strong&gt;, and &lt;a href="https://serpapi.com/" rel="noopener noreferrer"&gt;SerpApi&lt;/a&gt;, a Sales Assistant can combine internal CRM data with real-time market signals, producing actionable insights and personalized outreach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Static CRMs and the Context Gap
&lt;/h2&gt;

&lt;p&gt;Most CRMs are retrospective—they record historical interactions—but sales are forward-looking. Identifying high-potential leads requires real-time insights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the company hiring or expanding?&lt;/li&gt;
&lt;li&gt;Are there recent funding rounds or product launches?&lt;/li&gt;
&lt;li&gt;What new market trends might impact their priorities?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Previously, integrating live external signals into a CRM workflow required custom, complex code. While concepts like the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; aim to standardize such integrations, our Sales Assistant achieves the same outcomes using direct integrations with HubSpot and SerpApi, without relying on MCP.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Sales Assistant Works
&lt;/h2&gt;

&lt;p&gt;The assistant operates in a continuous &lt;strong&gt;Plan → Execute → Synthesize&lt;/strong&gt; loop, combining CRM data with external market intelligence.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Plan
&lt;/h3&gt;

&lt;p&gt;When asked, &lt;em&gt;“Prepare a briefing for my meeting with InnovateTech,”&lt;/em&gt; the agent evaluates available tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Internal:&lt;/strong&gt; Query HubSpot for interaction history, primary contacts, and deal stages.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External:&lt;/strong&gt; Search the web and news via SerpApi for recent company developments.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Execute
&lt;/h3&gt;

&lt;p&gt;The agent retrieves raw data from multiple sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HubSpot SDK:&lt;/strong&gt; Access company records, contact details, and engagement history.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SerpApi:&lt;/strong&gt; Pull recent news articles, funding announcements, and product updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Synthesize
&lt;/h3&gt;

&lt;p&gt;Using internal and external data, the agent generates personalized recommendations or outreach drafts:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Congratulations on your recent Series B funding. During our last conversation about cloud migration, you mentioned scaling challenges. With this new investment, we’d like to demonstrate how our solution can help support your upcoming initiatives.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Core Technical Components
&lt;/h2&gt;

&lt;p&gt;The complete implementation is available on GitHub. The Sales Assistant relies on three main components:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. HubSpot Native SDK
&lt;/h3&gt;

&lt;p&gt;Provides programmatic access to company and contact data, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hubspot_search_company_by_domain&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hubspot_get_contact_by_email&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hubspot_get_contact_activity_history&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. SerpApi Web and News Search
&lt;/h3&gt;

&lt;p&gt;Enables structured retrieval of relevant web content and news, with date filtering and normalized results for AI consumption.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. OpenAI LLM Integration
&lt;/h3&gt;

&lt;p&gt;Coordinates tool calls, synthesizes results, and drafts recommendations or outreach messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational Guidelines
&lt;/h2&gt;

&lt;p&gt;To maintain accuracy and reliability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source Attribution:&lt;/strong&gt; All external research is linked to original sources.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date Normalization:&lt;/strong&gt; Phrases like “last week” are converted to ISO date ranges before querying.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope Limitation:&lt;/strong&gt; HubSpot access is read-only for safety.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  MCP as an Alternative
&lt;/h2&gt;

&lt;p&gt;While the current implementation does not use MCP, it is a promising concept for standardizing AI access to multiple data sources. &lt;a href="https://serpapi.com/blog/introducing-serpapis-mcp-server/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; would allow models to interface with any API or database in a consistent manner.&lt;/p&gt;

&lt;p&gt;In this case, the agent achieves similar functionality through direct SDK and API integrations with HubSpot and SerpApi.&lt;/p&gt;

&lt;p&gt;The alternative implementation would rely on &lt;strong&gt;SerpApi MCP&lt;/strong&gt; and &lt;strong&gt;HubSpot MCP&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Observational CRMs
&lt;/h2&gt;

&lt;p&gt;Sales teams are moving from transactional CRMs, where humans manually enter data, to &lt;strong&gt;observational CRMs&lt;/strong&gt;, where AI agents monitor the market and update pipelines proactively.&lt;/p&gt;

&lt;p&gt;By integrating HubSpot, SerpApi, and OpenAI, research becomes part of the sales workflow. Sales teams can focus on building relationships with real-time intelligence, improving responsiveness, and reducing context-switching.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>sdr</category>
    </item>
    <item>
      <title>Integrating SerpApi MCP into Your Developer Workflow</title>
      <dc:creator>James Collins</dc:creator>
      <pubDate>Thu, 08 Jan 2026 15:47:43 +0000</pubDate>
      <link>https://dev.to/james_collins/integrating-serpapi-mcp-into-your-developer-workflow-8in</link>
      <guid>https://dev.to/james_collins/integrating-serpapi-mcp-into-your-developer-workflow-8in</guid>
      <description>&lt;p&gt;AI-assisted development tools such as Cursor and Claude have changed how developers interact with their codebases. These tools excel at generating, refactoring, and explaining code by leveraging the local project context. However, they are still limited when it comes to retrieving &lt;strong&gt;fresh, external information&lt;/strong&gt; such as updated documentation, security advisories, or real-world usage examples.&lt;/p&gt;

&lt;p&gt;The Model Context Protocol (MCP) enables developers to extend AI tools beyond static knowledge by connecting them to external services. By integrating custom MCP servers, developers can allow their AI assistants to query live data sources directly from the IDE. &lt;a href="https://serpapi.com/" rel="noopener noreferrer"&gt;SerpApi&lt;/a&gt; provides &lt;strong&gt;SerpApi MCP server&lt;/strong&gt;, which adds real-time web search capabilities to MCP-compatible AI tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending AI Tools with MCP
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://serpapi.com/blog/model-context-protocol-mcp-a-unified-standard-for-ai-agents-and-tools/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; defines a standard way for AI clients to call tools using structured requests. Instead of hardcoding integrations, MCP allows tools like search, databases, or internal APIs to be plugged into an AI assistant dynamically.&lt;/p&gt;

&lt;p&gt;In practice, this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Your AI assistant can decide when it needs external information&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It can call an MCP tool with a structured query&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The tool returns machine-readable results&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The assistant uses those results to improve its response&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For developers using VS Code, Cursor, or Claude Desktop, this turns the IDE into a live research environment rather than a closed system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the SerpApi MCP Server Provides
&lt;/h2&gt;

&lt;p&gt;The SerpApi MCP server exposes a single tool called &lt;code&gt;search&lt;/code&gt;. This tool allows AI assistants to perform live web searches using SerpApi and receive structured results. These results can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Documentation excerpts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Code examples&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Security advisories&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API usage discussions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tooling and configuration references&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI assistant can then reason over this data instead of guessing or relying on outdated training information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up SerpApi MCP
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Obtain a SerpApi API Key
&lt;/h3&gt;

&lt;p&gt;Create an account on the &lt;a href="https://serpapi.com/" rel="noopener noreferrer"&gt;SerpApi&lt;/a&gt; website and get an API key from your dashboard. This key is required to authenticate requests made through the MCP server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Add the MCP Server to Your AI Client
&lt;/h3&gt;

&lt;p&gt;SerpApi provides a hosted MCP endpoint, which is the simplest way to get started.&lt;/p&gt;

&lt;p&gt;For MCP-compatible clients such as Claude Desktop or Cursor, add the following configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"serpapi"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.serpapi.com/YOUR_SERPAPI_API_KEY/mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After saving the configuration, restart the AI tool. The SerpApi MCP server will now be available to the assistant.&lt;/p&gt;

&lt;p&gt;Alternatively, developers who prefer local control can clone the SerpApi MCP repository and run the server locally, then point their AI client to &lt;code&gt;http://localhost:8000&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the SerpApi Search Tool
&lt;/h2&gt;

&lt;p&gt;Once integrated, the AI assistant can call the SerpApi MCP Search tool using structured input. Below are hands-on examples that reflect real developer workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 1: Database Syntax Lookup
&lt;/h3&gt;

&lt;p&gt;A developer working with PostgreSQL wants to implement an UPSERT operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP tool call:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"q"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PostgreSQL UPSERT ON CONFLICT example"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result usage:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The assistant retrieves current syntax examples and produces a valid SQL snippet using &lt;code&gt;INSERT ... ON CONFLICT DO UPDATE&lt;/code&gt;, tailored to the developer’s schema.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example 2: Security Vulnerability Investigation
&lt;/h3&gt;

&lt;p&gt;A dependency audit flags a potential vulnerability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP tool call:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"q"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CVE-2023-4863 vulnerability details mitigation"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result usage:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The assistant summarizes the vulnerability, affected versions, and mitigation steps, helping the developer decide whether an upgrade or patch is required.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example 3: Language Syntax and Code Patterns
&lt;/h3&gt;

&lt;p&gt;A developer needs to set a timeout for a JavaScript fetch request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP tool call:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"q"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JavaScript fetch timeout AbortController example"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result usage:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The assistant composes a correct code example using &lt;code&gt;AbortController&lt;/code&gt;, based on real-world usage patterns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example 4: Tooling and Configuration Questions
&lt;/h3&gt;

&lt;p&gt;A developer is configuring Docker and needs clarification on &lt;code&gt;CMD&lt;/code&gt; vs &lt;code&gt;ENTRYPOINT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP tool call:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"q"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Docker CMD vs ENTRYPOINT differences"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result usage:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The assistant explains the behavioral differences and provides examples of when to use each directive.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example 5: Ecosystem and Version Awareness
&lt;/h3&gt;

&lt;p&gt;Before upgrading a dependency, a developer wants to understand recent changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP tool call:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"q"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"React 19 breaking changes"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result usage:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The assistant summarizes notable changes and highlights potential migration concerns relevant to the existing codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By integrating the SerpApi MCP server into AI-assisted development tools, developers unlock real-time information retrieval directly inside their IDEs. This approach reduces context switching, improves accuracy, and allows AI assistants to operate with up-to-date knowledge.&lt;/p&gt;

&lt;p&gt;As MCP tooling becomes more widely adopted, AI-driven development is likely to shift from static code generation toward more adaptive, tool-augmented workflows. Search-enabled MCP servers such as one provided by SerpApi are an early example of how external knowledge can be seamlessly integrated into everyday software development, ultimately improving productivity and decision-making across the development lifecycle.&lt;/p&gt;

</description>
      <category>devex</category>
      <category>devworkflow</category>
      <category>mcp</category>
    </item>
    <item>
      <title>AI-Powered SEO Research Agent with OpenAI &amp; SerpApi</title>
      <dc:creator>James Collins</dc:creator>
      <pubDate>Wed, 10 Sep 2025 13:10:41 +0000</pubDate>
      <link>https://dev.to/james_collins/ai-powered-seo-research-agent-with-openai-serpapi-390g</link>
      <guid>https://dev.to/james_collins/ai-powered-seo-research-agent-with-openai-serpapi-390g</guid>
      <description>&lt;p&gt;Search engines and AI are rapidly reshaping how businesses find opportunities online. Today’s “AI agents” – systems that autonomously browse and query the web on a user’s behalf – are already changing SEO best practices. For example, industry data shows that ChatGPT’s user agents doubled their web-search activity in July 2025, fundamentally altering how sites need to be discovered and indexed . At the same time, language models alone cannot know the latest trends or live keyword data. &lt;/p&gt;

&lt;p&gt;To bridge this gap, we built a &lt;strong&gt;SEO Research Agent&lt;/strong&gt;: a chat-based assistant that combines OpenAI’s new function-calling with SerpApi’s Google search tools. It plans queries, gathers live SERP data, and synthesizes a full &lt;strong&gt;cited SEO report&lt;/strong&gt; – giving marketers up-to-date insights into keywords, competitors, and news-driven content ideas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use an AI Agent for SEO Research
&lt;/h2&gt;

&lt;p&gt;SEO research often means chasing fresh, authoritative information. For instance, effective keyword discovery relies on current autocomplete suggestions and competitor SERPs . Google Autocomplete is one of the most accurate keyword research tools for real-time ideas . Likewise, staying ahead in content strategy requires monitoring industry news. However, manually collecting this data is tedious. That’s where an agentic AI comes in. &lt;/p&gt;

&lt;p&gt;By combining a language model’s reasoning with automated search tools, our agent can &lt;strong&gt;batch together keyword queries, competitor analysis, and news searches&lt;/strong&gt;, then distill the results into a concise report. This “plan → execute → synthesize” workflow is an established pattern for multi-tool AI agents . &lt;/p&gt;

&lt;p&gt;The model first &lt;strong&gt;plans&lt;/strong&gt; all needed searches, the system &lt;strong&gt;executes&lt;/strong&gt; them in parallel (using SerpApi to avoid captchas and proxy issues), and then the agent &lt;strong&gt;synthesizes&lt;/strong&gt; the findings into a structured answer . In practice, this means our SEO agent automatically collects live SERP snippets, autocomplete suggestions, ranking positions, and recent headlines – all without extra work from the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the SEO Research Agent Works
&lt;/h2&gt;

&lt;p&gt;The agent is implemented in Python as a conversational assistant. It uses OpenAI’s API with &lt;strong&gt;function-calling tools&lt;/strong&gt;. We’ve defined four main tools based on SerpApi endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;search_web&lt;/strong&gt; – Runs a Google organic search for a query, returning the top titles and snippets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;search_autocomplete&lt;/strong&gt; – Calls Google Autocomplete to list related keyword suggestions (long-tail terms).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;search_news&lt;/strong&gt; – Queries Google News for top headlines and snippets on a topic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;check_rank&lt;/strong&gt; – Finds the ranking position of a given domain for a specific keyword (scanning Google’s top 100 results).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tools are exposed to the language model through a system prompt that enforces an iterative research loop. In essence: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phase 1:&lt;/strong&gt; the model writes a natural-language &lt;em&gt;plan&lt;/em&gt;, describing which keywords and data it needs (for example, “we’ll start by gathering autocomplete suggestions for the brand name, then list competitors from the SERP, and finally check our rank on those terms.”). &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 2:&lt;/strong&gt; the model emits a batch of structured tool calls in one JSON object. Each call has an ID, name, and arguments (e.g. &lt;code&gt;{id: "c1", type: "function", function: { name: "search_autocomplete", arguments: {"query": "ai SEO tools"} }}&lt;/code&gt;), which the host executes in parallel. This approach dramatically reduces latency and ensures comprehensive coverage. Once all tool calls have run, their results (concise title:snippet lists, or ranking positions) are returned to the model as “tool” messages. The agent may iterate: if the first set of results suggests new queries (perhaps additional keywords or competitor sites), the model can plan a second round of tool calls. This loop continues until the model has gathered enough information. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 3:&lt;/strong&gt; the model generates the final &lt;strong&gt;SEO Report&lt;/strong&gt; in Markdown. This report includes sections like &lt;strong&gt;Keyword Opportunities&lt;/strong&gt;, &lt;strong&gt;SERP Insights&lt;/strong&gt;, &lt;strong&gt;Domain Ranking&lt;/strong&gt;, and &lt;strong&gt;News &amp;amp; Topical Opportunities&lt;/strong&gt;, each formatted with bullets and embedded citations from the tool outputs. By following this explicit plan–execute–synthesize cycle, the agent provides a transparent, auditable research process .&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Tools &amp;amp; Capabilities
&lt;/h2&gt;

&lt;p&gt;The SEO agent’s power comes from integrating SerpApi’s search data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real-time SERP Data:&lt;/strong&gt; By calling SerpApi’s Google Search API, the agent gets &lt;strong&gt;organic results&lt;/strong&gt;, snippets, and related searches. As Nimbleway explains, a SERP API “scrapes and retrieves data from search engine results pages” and can return organic results, ads, snippets, URLs, knowledge graph data, and related searches  . We use this to identify competitor domains, featured snippets, and keyword contexts. For example, &lt;code&gt;search_web&lt;/code&gt; might reveal that “brightdata.com” and “apify.com” are top competitors for web scraping APIs, with specific snippet text that the report can cite.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keyword Suggestions:&lt;/strong&gt; The &lt;code&gt;search_autocomplete&lt;/code&gt; tool taps Google Autocomplete for suggestions on each seed keyword or brand name. This yields dozens of long-tail and related keyword ideas. Experts note that starting with Google suggestions is a core keyword research technique . In fact, “Google Autocomplete is the most accurate keyword research tool” for uncovering current search queries . Our agent formats these suggestions into bullet lists for the &lt;strong&gt;Keyword Opportunities&lt;/strong&gt; section.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;News &amp;amp; Trends:&lt;/strong&gt; The &lt;code&gt;search_news&lt;/code&gt; function uses SerpApi’s Google News API to fetch recent headlines relevant to the topic. Monitoring news is crucial for timely SEO content: as RapidSeedBox points out, you can “keep up with industry news” by pulling data from Google News and spotting brand mentions in real time . In the report’s &lt;strong&gt;News &amp;amp; Topical Opportunities&lt;/strong&gt; section, the agent highlights any breaking stories or trending angles related to the keywords (with source citations).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rank Checking:&lt;/strong&gt; Finally, the &lt;code&gt;check_rank&lt;/code&gt; tool runs a Google search for a keyword and scans the results for the target domain. It reports the rank position (or notes if the domain is not in the top results). According to SEO guides, rank tracking is one of the most common uses of SERP APIs, since it automates the tedious task of checking positions . Our agent uses this to populate the &lt;strong&gt;Domain Ranking&lt;/strong&gt; section: e.g. “example.com ranks #4 for ‘best coffee grinder’ and is not in the top 50 for ‘grinder maintenance tips’.”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Behind the scenes, the code parallelizes these API calls for speed. SerpApi handles proxy rotation and CAPTCHAs, so we get reliable, structured JSON results for each query. The agent then extracts just titles, snippets, or rank numbers, keeping the returned context concise for the model to digest. This combination of tools means the agent covers broad SEO research tasks (keyword ideation, competitor scan, ranking analysis, trend spotting) in one multi-step workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example SEO Report Output
&lt;/h2&gt;

&lt;p&gt;After gathering data, the agent writes a cohesive report in Markdown. The report generally follows this structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keyword Opportunities:&lt;/strong&gt; A bullet list of long-tail keywords and related terms from autocomplete. For example: &lt;code&gt;- "brand X tutorial"&lt;/code&gt;, &lt;code&gt;- "brand X vs competitors"&lt;/code&gt;, etc. These come from the &lt;code&gt;search_autocomplete&lt;/code&gt; results, and focus on phrases that real users are searching for.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SERP Insights:&lt;/strong&gt; Highlights of the web search results. It might say, for example, “Competitor: &lt;code&gt;apify.com&lt;/code&gt; – offers web scraping &amp;amp; automation tools .” (Here we might cite a relevant snippet from the search results). The agent points out high-ranking competitors, identifies whether featured snippets or rich cards appear, and notes any content gaps. As one AI-SEO guide explains, specialized agents can “analyze the data by themselves and provide...actionable insights” rather than just raw numbers . Our agent does the same for SERP analysis.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Domain Ranking:&lt;/strong&gt; A list showing where the target domain ranks for each keyword. E.g. &lt;code&gt;- example.com ranks #1 for "primary keyword"&lt;/code&gt; or “not in top 50” for weaker terms. These come from the &lt;code&gt;check_rank&lt;/code&gt; outputs. The report may include all keywords checked (often 5–10 or more) to give a clear picture of current SEO standing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;News &amp;amp; Topical Opportunities:&lt;/strong&gt; Any timely articles or news items. For instance: &lt;code&gt;"New AI agents in search workflows" – recent news item (source)&lt;/code&gt;. This section is drawn from the &lt;code&gt;search_news&lt;/code&gt; results. The agent treats emerging trends as SEO opportunities, noting angles that content creators could exploit. (RapidSeedBox notes that pulling Google News is great for tracking brand mentions and breaking stories in real time .)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Recommendations:&lt;/strong&gt; Based on the analysis, the agent may add suggested actions (e.g. target long-tail keywords, improve content around topics where competitors rank, etc.). These are generated by the model using the collected data. Everything is written in a clear, business-friendly tone with inline citations back to the search snippets.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final report is easy to read and can even be published internally or shared. All key claims are traceable to the underlying SERP data (for example, competitor mentions are accompanied by “[Source]” linking to the snippet’s origin). This makes the process auditable and transparent – a direct benefit of using an agentic approach where each fact comes from a known search query .&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;The SEO Research Agent runs locally via Python. Setup is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install requirements:&lt;/strong&gt; Python 3.9+ and &lt;code&gt;pip install openai serpapi&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set API keys:&lt;/strong&gt; Provide your &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; and &lt;code&gt;SERPAPI_API_KEY&lt;/code&gt; (SerpApi has a free tier with 250 searches/month for testing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the agent:&lt;/strong&gt; You can use the CLI (&lt;code&gt;python seo_agent.py -q "SEO analysis for example.com"&lt;/code&gt;) or import the &lt;code&gt;SEOResearchAgent&lt;/code&gt; class in your code. It supports interactive mode as well: just run without &lt;code&gt;-q&lt;/code&gt; to chat.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python seo_agent.py &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"SEO report for vanta.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;will prompt the agent to produce a full SEO report for the given domain. The conversation trace can be saved in JSON (&lt;code&gt;--outfile trace.json&lt;/code&gt;) for debugging or auditing purposes.&lt;/p&gt;

&lt;p&gt;Under the hood, the code follows the plan–tool–report loop described above. The system prompt guides the model to first plan the research (listing data to collect), then emit all tool calls together, then refine if needed, and finally output the Markdown report. This matches best practices for multi-tool AI agents  and ensures the agent doesn’t stop at shallow answers. Instead, it iterates until it confidently has comprehensive data for all sections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By combining OpenAI’s latest models with SerpApi’s real-time search APIs, the SEO Research Agent brings cutting-edge AI to everyday SEO workflows. It automates keyword brainstorming, competitor audits, rank tracking, and news monitoring – tasks that normally take hours of manual search and curation. &lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>Building Ahead: a multi-tool AI Travel Agent with OpenAI + SerpApi</title>
      <dc:creator>James Collins</dc:creator>
      <pubDate>Fri, 29 Aug 2025 12:16:40 +0000</pubDate>
      <link>https://dev.to/james_collins/building-ahead-a-multi-tool-ai-travel-agent-with-openai-serpapi-1ce4</link>
      <guid>https://dev.to/james_collins/building-ahead-a-multi-tool-ai-travel-agent-with-openai-serpapi-1ce4</guid>
      <description>&lt;p&gt;Modern travel planning demands fresh, structured signals and predictable automation. Flight availability, hotel inventory, and local recommendations are time-sensitive and often encoded in vertical search outputs. AI agents can directly assist in solving this problem. A reliable AI travel agent will combine the reasoning capabilities of a language model with direct access to different search APIs. &lt;/p&gt;

&lt;p&gt;This post describes a follow-up to the research agent pattern (covered in prior AI agent blog): a travel planning agent that plans its retrievals, verifies structured inputs (IATA codes), runs targeted vertical searches (Flights, Hotels, Local/Maps, Web), and synthesizes concise, cited itineraries.&lt;/p&gt;

&lt;p&gt;Full code available here: &lt;a href="https://github.com/serpapi/travel-planning-agent" rel="noopener noreferrer"&gt;https://github.com/serpapi/travel-planning-agent&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Use cases
&lt;/h2&gt;

&lt;p&gt;The travel agent’s goal is to help travelers discover new destinations faster and plan trips better. It also very cheap to run in comparison to hiring a travel agency or travel advisors.  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Itinerary assembly:&lt;/strong&gt; curate flight options, hotel availability, possible travel experiences and return a brief, actionable plan with source links.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discovery workflows:&lt;/strong&gt; surface destination ideas when only soft constraints are provided (season, tone, budget).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraint-driven planning:&lt;/strong&gt; honor explicit limits (max price, cabin class, passenger mix, all-inclusive) and produce ranked options.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit and reproducibility:&lt;/strong&gt; save a JSON trace of the model’s planned tool calls and returned snippets for debugging, compliance, or user review.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each use case benefits from the same core pattern: explicit planning by the model, concurrent retrieval by the host, and a single synthesis step that ties everything together with citations.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works — high level
&lt;/h2&gt;

&lt;p&gt;Three coordinated stages form the backbone of the agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plan.&lt;/strong&gt; The model emits a batch of structured tool calls that enumerate the data required — IATA lookups, candidate flight date windows, hotel queries for target neighborhoods, and local POI searches. The set of tool calls is produced before any external requests are executed so coverage is explicit and auditable.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute.&lt;/strong&gt; The host executes the model’s tool calls. Independent calls are dispatched concurrently to reduce latency (thread pool or async execution). Each tool returns compact, structured snippets (title/snippet/price/link) which are appended to the conversation as tool messages and associated with the originating tool_call_id.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synthesize.&lt;/strong&gt; With the retrieved snippets in context the model composes a final answer: a concise itinerary, ranked flight/hotel options, local recommendations, and footnote-style citations that map directly to the returned URLs.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This plan → execute → synthesize loop preserves an auditable trace and minimizes token waste while ensuring the model reasons over current, structured data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.9+
&lt;/li&gt;
&lt;li&gt;Environment variables: &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;, &lt;code&gt;SERPAPI_API_KEY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Libraries: &lt;code&gt;openai&lt;/code&gt; (or the official OpenAI SDK used for function/tool calls) and &lt;code&gt;serpapi&lt;/code&gt; for vertical search access
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can install necessary libraries using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can obtain SerpApi API key on the SerpApi website. There is a free plan that offers 250 searches / month, so you can freely test the agent.  &lt;/p&gt;

&lt;p&gt;A CLI wrapper supports interactive chat and one-shot queries and can optionally persist the full JSON trace for auditing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key operational rules
&lt;/h2&gt;

&lt;p&gt;The agent follows a small set of explicit operational rules encoded in the system prompt and enforced by the host:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IATA verification:&lt;/strong&gt; the flights tool requires 3-letter IATA codes. Before calling the flights endpoint the model must resolve and verify airport codes via a web lookup (e.g., "Warsaw IATA code → WAW"). This prevents invalid flight queries and reduces tool errors.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date disambiguation:&lt;/strong&gt; ambiguous dates are interpreted as future travel. If a referenced month has already passed in the current year, the agent interprets it as next year. For vague phrasing such as “mid-May,” the agent probes a small date window (for example 13–17 May) and searches multiple candidate windows rather than a single day.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasonable defaults:&lt;/strong&gt; when the user omits details, the agent assumes sensible defaults and surfaces them in the response (example defaults: economy cabin, up to one stop, 2 adults, ±3 days flexibility). Tone in the query (e.g., “luxury”) adjusts defaults (premium cabins, higher star hotels).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batching and parallelism:&lt;/strong&gt; the model should emit all needed tool calls in a single assistant message when multiple external queries are required. The host executes independent calls concurrently to reduce latency.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparency and citations:&lt;/strong&gt; final outputs include footnote-style citations that link back to the URLs returned by the vertical APIs. The JSON trace preserves tool_call_id → result associations for reproducibility.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Encoding these rules as part of the system prompt plus light host-side validation produces predictable, auditable behavior and fewer failed calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tooling integration
&lt;/h2&gt;

&lt;p&gt;The model is equipped with multiple tools that it has access to. For each tool a schema is defined that the model should output in order to request results from a certain tool. Below we show Google Flights tool integration, since it's one of the core tools to plan the travel. Other tools are integrated in a similar fashion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Representative&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;schema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(flights)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;host-side&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;mapping&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(conceptual)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Model-facing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;schema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(what&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;LLM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;can&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;call)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search_flights"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Find flights given IATA airport codes and dates (departure_date required)."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"departure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"departure_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YYYY-MM-DD"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"return_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YYYY-MM-DD (optional)"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"cabin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"max_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"departure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"departure_date"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Host-side behavior (conceptual):
# 1) Validate IATA codes (3 letters). If not present, run search_web to resolve.
# 2) Normalize dates and params.
# 3) Call SerpApi Flights engine:
#    params = {"engine":"google_flights","api_key":SERPAPI_API_KEY,
#              "departure_id":dep_code,"arrival_id":arr_code,"outbound_date":departure_date, ...}
# 4) Normalize returned results to: {price, total_duration_min, legs, link}
# 5) Append normalized JSON to conversation as a `tool` message with the tool_call_id.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This integration pattern enforces a clear contract: the model requests flights using the declared schema, the host validates and materializes the request against SerpApi, and the model receives compact, normalized results suitable for synthesis and citation.  &lt;/p&gt;

&lt;p&gt;The following tools are integrated. Each tool is mapped to a specific SerpApi endpoint.  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Flights&lt;/strong&gt; (&lt;code&gt;engine=google_flights&lt;/code&gt;) yields structured flight listings (price, duration, legs) and a canonical flights result URL.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Hotels&lt;/strong&gt; (&lt;code&gt;engine=google_hotels&lt;/code&gt;) returns property metadata, ratings, and snippet links to booking surfaces.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Local/Maps&lt;/strong&gt; (&lt;code&gt;engine=google_local&lt;/code&gt;) supplies POIs, ratings, types, and map links.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google organic&lt;/strong&gt; (&lt;code&gt;engine=google&lt;/code&gt;) supports general web lookups (IATA lookups, policy pages, travel advisories).
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach to integration keeps token consumption predictable; an optional “deep scrape” tool can be added later for full-page context when necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inference loop
&lt;/h2&gt;

&lt;p&gt;The inference loop is structured in the following sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Append system prompt and user message to the conversation.
&lt;/li&gt;
&lt;li&gt;Request a model completion with the function/tool schema.
&lt;/li&gt;
&lt;li&gt;If the model returns a tool_calls assistant message, append it and execute those calls:

&lt;ul&gt;
&lt;li&gt;Validate dependent calls (for example, IATA resolution) before invoking dependent tools.
&lt;/li&gt;
&lt;li&gt;Run non-dependent calls concurrently; append results as tool messages associated with their tool_call_id.
&lt;/li&gt;
&lt;li&gt;Reinvoke the model to synthesize a final answer using the returned snippets.
&lt;/li&gt;
&lt;li&gt;If the model requests further tool calls, repeat; otherwise return the final synthesis.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Preserving the ordering and identifiers between tool calls and results is essential for accurate citations and for saving a reproducible JSON trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Behavior, defaults and error handling
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assumptions must be surfaced.&lt;/strong&gt; Any default assumptions used to produce results are shown in the final answer (for example: “assumed economy, 2 adults, flexible ±3 days”). If the missing detail would materially change results (passenger mix, max price), the agent asks a concise clarifying question rather than guessing.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache small, stable lookups.&lt;/strong&gt; IATA lookups, frequent POIs, and static data should be cached locally to reduce API usage and speed repetitive queries.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quota and rate limiting.&lt;/strong&gt; Batched concurrent retrievals reduce latency but increase burst usage. Implement simple rate limiting or token bucket strategies around SerpApi calls to avoid quota issues.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful tool errors.&lt;/strong&gt; If a tool returns an error (invalid arguments, rate limit), the host returns a short structured error snippet to the model and allows the model to either retry with corrected arguments or ask the user for clarification.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example flow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;User query:&lt;/strong&gt; Weekend in Rome mid-May, leaving Lisabon.  &lt;/p&gt;

&lt;p&gt;Planner (model) emits a small batch of tool calls:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;search_web("Lisabon IATA code")&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;search_web("Rome IATA code")&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;search_hotels(destination="Rome", check_in=2026-05-15, check_out=2026-05-17)&lt;/code&gt; (candidate window)
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;search_places(query="Colosseum Rome", location="Rome", limit=5)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;several &lt;code&gt;search_flights&lt;/code&gt; calls for 2–3 Friday→Sunday windows once IATA codes are resolved
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Executor runs IATA lookups first, then runs hotels and places concurrently while issuing the validated flight calls. Results are returned as compact snippets. Synthesizer writes a short itinerary with three flight options, two hotel picks, and local highlights, each with footnote links to the SerpApi results. The response explicitly notes assumed defaults and suggests refinement steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLI and programmatic usage
&lt;/h2&gt;

&lt;p&gt;A small CLI wrapper supports two modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interactive chat&lt;/strong&gt; for iterative discovery and follow-up refinement.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-shot query&lt;/strong&gt; for quick plans, with an option to persist the JSON trace (&lt;code&gt;--outfile&lt;/code&gt;) for auditing.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Typical commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# interactive&lt;/span&gt;
python travel_planning_agent.py

&lt;span class="c"&gt;# one-shot, save trace&lt;/span&gt;
python travel_planning_agent.py &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"Luxury honeymoon Bali December"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; trace.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--debug&lt;/code&gt; exposes intermediate tool calls and SerpApi responses for prompt tuning and debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and future extension points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Booking flow:&lt;/strong&gt; moving from planning to booking requires partner APIs and additional compliance considerations (payment, PII handling).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Richer passenger modeling:&lt;/strong&gt; support for infants, children, special assistance and multi-party trips increases complexity in passenger constraints and pricing models.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personalization:&lt;/strong&gt; persist user preferences (airlines, seat class, hotel loyalty numbers) to bias search and filter results.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explainability UI:&lt;/strong&gt; render the JSON trace with click-through snippets so end users or auditors can inspect every tool call and its returned evidence.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broader data sources and destinations:&lt;/strong&gt; to create a really powerful agent capable of planning your next trip, expansion of data sources will be necessary. Integration of cruise lines, private transfers, travel perk / discount discovery, better coverage of non-default travel options such as road trips, themed parks, natural parks, etc. Coverage of additional options like travel insurance, upgrades, group travel, etc. are all necessary to create a high-quality getaway planner.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The research-agent pattern (plan → execute → synthesize) adapts naturally to travel planning when combined with vertical search APIs. Encoding a small set of operational rules (IATA verification, date logic, batching) and returning compact, cited snippets produces reliable, actionable itineraries with an auditable trace. Batched planning plus concurrent execution reduces latency and improves coverage, while proper validation and caching reduce failures and API costs. The resulting travel planner agent provides a practical foundation for booking integrations, personalization, and richer optimization in future work.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>Building a fast, self‑hosted research agent with OpenAI models + SerpAPI</title>
      <dc:creator>James Collins</dc:creator>
      <pubDate>Thu, 14 Aug 2025 10:59:52 +0000</pubDate>
      <link>https://dev.to/james_collins/building-a-fast-self-hosted-research-agent-with-openai-models-serpapi-14lp</link>
      <guid>https://dev.to/james_collins/building-a-fast-self-hosted-research-agent-with-openai-models-serpapi-14lp</guid>
      <description>&lt;p&gt;Modern language models are effective at synthesis but do not inherently provide fresh, verifiable information. Connecting a model to the web search creates an autonomous AI agent that closes the gap: it enables current sources, systematic coverage of a topic, and traceable answers. This post describes a compact research agent that plans its searches, executes them concurrently via SerpAPI, and produces a cited synthesis—designed to be readable, auditable, and simple to run locally.&lt;/p&gt;

&lt;p&gt;Full code available here: &lt;a href="https://github.com/serpapi/web-research-agent" rel="noopener noreferrer"&gt;link&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use cases for AI research agents
&lt;/h2&gt;

&lt;p&gt;The AI agent targets questions where recency and attribution matter. It streamline research and can automate many repeatable workflows and complex tasks: market scans, literature overviews, competitive comparisons, and quick technical surveys. Instead of incremental browsing, the model first enumerates all searches it needs, the system executes those queries in parallel, and the model then writes a grounded answer using the returned snippets. Since the agent has access to web on the fly, the results are close to real-time.&lt;/p&gt;

&lt;p&gt;Among different use cases, the web research agents in particular allow for scalable data collection of online datasets. The agent’s outputs could be streamed into natural language processing pipelines for further generation of insights. Those insights could be stored in a knowledge base or further refined via more complex systems, such as multi-agent systems where several agents collect data and other agents conduct it’s review.&lt;/p&gt;

&lt;p&gt;Organizations could use web research agents for internal uses too, such as using them to conduct research on customer data to enhance customer experience and improve customer support.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works (overview)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Planning: Large language model emits a batch of structured tool calls, each containing a Google query.&lt;/li&gt;
&lt;li&gt;Execution: The agent runs those queries concurrently through SerpAPI and returns concise title:snippet pairs as tool messages.&lt;/li&gt;
&lt;li&gt;Synthesis: With the results in context, the model produces the final answer, including inline citations derived from the snippets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern keeps latency low (parallel requests), improves coverage (the plan precedes the data), and preserves a step-by-step trace for auditing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.9+ (3.10+ recommended)&lt;/li&gt;
&lt;li&gt;Environment variables: OPENAI_API_KEY and SERPAPI_API_KEY&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can obtain OpenAI API key at &lt;a href="https://platform.openai.com/" rel="noopener noreferrer"&gt;OpenAI platform website&lt;/a&gt;. For SerpAPI key, you can register at &lt;a href="https://serpapi.com/" rel="noopener noreferrer"&gt;SerpApi website&lt;/a&gt;. There is a free plan so you can test the agent first. Then install the repository run the agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/vladm-serpapi/web-research-agent
&lt;span class="nb"&gt;cd &lt;/span&gt;web-research-agent
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# set keys in shell (recommended)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk-..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;

python research_agent.py &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"What are the latest approaches to retrieval‑augmented generation in 2025?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;The agent is implemented as a single class with a run method. The constructor initializes model configuration, API clients, the tool schema, and the system prompt. Model configuration accepts different AI models, but primarily the ones supported by OpenAI. The code could be extended to optimize this and allow for provider-agnostic inference (e.g. using OpenRouter). Once the agent is built, the run method executes the inference loop until a final answer is produced.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# research_agent.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResearchAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;LLM‑powered researcher that combines OpenAI o‑series model with SerpAPI.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;o3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;topn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;openai_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;serpapi_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;topn&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;debug&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;openai_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai_key&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serp_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serpapi_key&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;openai_key&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serp_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY and SERPAPI_API_KEY must be set.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;openai_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# tools + prompt initialized below ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to actually connect to the Web, we need to provide the model with a tool to do so. &lt;code&gt;self.tools&lt;/code&gt; field is initialized with a tool schema that the model will generate when it needs to get the web data. Tool schema definition includes the function name, tool description and the parameter that we want the model to provide. In this case we ask the model to just provide query: string parameter for Google Search. Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_web&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Search Google and return the top result snippets.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parameters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Google search string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system prompt is defined in a way that encourages the model to research the user's question using the &lt;code&gt;search_web&lt;/code&gt; tool. The prompt requires the model to generate all necessary tool calls in a batch and return them together. This tool batching is necessary to improve the total processing latency. If the model needs to make say 10 requests, then each request will take 1 second, then it will take a total of 10 seconds. If we use batching and run all the calls concurrently, then we will reduce the total run time to just 1 second (ideal case).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sys_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a meticulous research assistant.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;When outside knowledge is needed, you must emit ALL `search_web` tool calls &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in a SINGLE assistant message before reading any results.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You must return them in the exact JSON structure the API expects for `tool_calls`,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;with each having its own `id`, `type`, and `function` fields.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Do not write explanations, just the tool calls.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Always batch between 2 and 50 calls in a single turn if you need external data.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Only after all tool outputs are returned should you write your final, well-cited answer.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice, this nudges the model to enumerate queries that span the topic (overview, specifics, recent updates) before seeing any retrieved snippets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The retrieval backend (SerpAPI integration)
&lt;/h2&gt;

&lt;p&gt;Each tool call is executed against SerpAPI’s Google endpoint. The agent returns compact title:snippet pairs, which are sufficient for grounding and efficient on tokens. We use SerpAPI Python SDK in order to make requests and obtain the result snippets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_search_web&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[DEBUG] → SerpAPI query: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GoogleSearch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serp_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topn&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;org&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_dict&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;organic_results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])[:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(untitled)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;snippet&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(no snippet)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No results found.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returning concise strings instead of full pages keeps interactions predictable and reduces overhead. It is possible to extend this implementation in the future, to include a scraper tool that will fetch the complete pages. That could allow the model to generate better insights by getting access to more context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agentic inference loop
&lt;/h2&gt;

&lt;p&gt;At the top level, run(question: str) builds the message context (system + user), calls the chat completions API, and branches depending on whether the model returned tool calls or a final answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sys_prompt&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[DEBUG] → OpenAI chat.completions.create request …&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tool_choice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# append assistant message FIRST (per API contract)
&lt;/span&gt;            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# fetch all tool results concurrently
&lt;/span&gt;            &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_search_web&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

            &lt;span class="c1"&gt;# append tool results in the same order as tool_calls
&lt;/span&gt;            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
                &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_call_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;  &lt;span class="c1"&gt;# next iteration → model now has snippets; produce final answer
&lt;/span&gt;
        &lt;span class="c1"&gt;# no tool calls → final answer
&lt;/span&gt;        &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant_answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;steps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points in the loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent always appends the assistant’s tool_calls message before returning tool outputs (API contract).&lt;/li&gt;
&lt;li&gt;Tool execution is concurrent via ThreadPoolExecutor to reduce latency.&lt;/li&gt;
&lt;li&gt;Tool outputs are appended in order and associated with tool_call_id; the next model call then has everything needed to synthesize the answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Putting it all together
&lt;/h2&gt;

&lt;p&gt;Finally, a small wrapper exposes the agent for use in the terminal. It parses flags, instantiates ResearchAgent, runs it, prints the final answer, and optionally writes a JSON trace (steps + answer) for auditing. The common argparse library is used to do that. The wrapper exposes then necessary options for the client, such as &lt;code&gt;-q&lt;/code&gt; - provide a question, &lt;code&gt;-m&lt;/code&gt; - provide the model name, &lt;code&gt;--outfile&lt;/code&gt; - specifies file for results, &lt;code&gt;--debug&lt;/code&gt; - enables debug logging to trace intermediate execution steps.&lt;/p&gt;

&lt;p&gt;The result is a compact, auditable pipeline: the model is doing the planning and generates search requests, search requests are retrieved in parallel via SerpAPI,  the model generates an answer with citations - all within a few clear components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;The code could be used either via the terminal or imported and used directly as a Python module. Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# basic&lt;/span&gt;
python research_agent.py &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"State of LLM reasoning benchmarks in 2025"&lt;/span&gt;

&lt;span class="c"&gt;# save a JSON trace (tool calls, results, final answer)&lt;/span&gt;
python research_agent.py &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"Compare FAISS vs. Milvus vs. Qdrant for RAG (2025)"&lt;/span&gt; &lt;span class="nt"&gt;--outfile&lt;/span&gt; trace.json

&lt;span class="c"&gt;# control model and results per search&lt;/span&gt;
python research_agent.py &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"Airline industry trends in 2025"&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; gpt-4o &lt;span class="nt"&gt;-n&lt;/span&gt; 8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using in Python directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from research_agent import ResearchAgent

agent = ResearchAgent(model="gpt-4o", topn=10, debug=False)
result = agent.run("Summarize the most cited papers on RAG.")
print(result["answer"])  # final, cited summary
print(len(result["steps"]))  # trace length
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes on the usage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keys: ensure both OPENAI_API_KEY and SERPAPI_API_KEY are available in the environment before running the scripts.&lt;/li&gt;
&lt;li&gt;Model behavior: o3 / o4‑mini may prefer fewer tool calls per turn; gpt‑4o often batches more queries when broad coverage is required.&lt;/li&gt;
&lt;li&gt;Model’s output: as usual with all AI models hallucination is possible. Human oversight is required to ensure the output quality.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion and what's next
&lt;/h2&gt;

&lt;p&gt;In this blog post, we showed how to design an Agent capable of running multiple complex research workflows, such as: news scans, literature reviews, vendor/technology comparisons, and quick technical surveys where recency and traceability matter. It runs locally via a CLI or as a library, with optional JSON traces to audit tool calls and outputs. &lt;/p&gt;

&lt;p&gt;For future blog posts, we plan to expand the Agent's functionality to include multiple tools (maps, flights, hotels, domain APIs, etc.) to support more complex, goal-directed workflows. We will experiment with different AI tools and generative AI frameworks to build an Agent capable of a more complex decision making.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>tutorial</category>
      <category>aiagents</category>
    </item>
  </channel>
</rss>
