DEV Community

Cover image for Building a Full-Stack AI Shopping Assistant with CrewAI and Tavily
Bonnie for CopilotKit

Posted on

Building a Full-Stack AI Shopping Assistant with CrewAI and Tavily

TL;DR

In this guide, you will learn how to build a full-stack AI shopping assistant using CrewAI Flows, paired with the Tavily Search API in the backend. We'll then walk through adding a frontend to the shopping agent using CopilotKit to interact with it.

The AI shopping assistant will be able to search products on commerce platforms such as Amazon, Target, or eBay- extract structured product data and provide tailored product recommendations.

Before we jump in, here is what we will cover:

  • What are CrewAI Flows?

  • Building the AI shopping assistant backend using CrewAI, Tavily, and CopilotKit

  • Building the AI shopping assistant frontend using CopilotKit.

Here is a preview of what we will be building:

What are CrewAI Flows?

Flows are a flexible building block in CrewAI that lets you control things at a detailed level while keeping it simple overall. You can use them to automate tasks ranging from a basic call to an AI model all the way to a team of smart AI agents that work on their own.

With Flows, it's straightforward to create and set up step-by-step processes that make the most of what CrewAI can do.

Here are some key features:

  • Easy Workflow Building: Quickly link up different teams of AI agents (called Crews) and individual tasks to make advanced AI systems.

  • Handling Shared Information: Flows make it really simple to keep track of and share data between various steps in your process.

  • Event-Based Design: It's built around reacting to events, which helps create workflows that adapt and respond quickly.

  • Customizable Paths: Add rules like "if this happens, then do that," repeat steps in loops, or split into different branches in your workflows.

You can learn more about CrewAI Flows here on CrewAI docs.

Image from Notion

Now that we have learned what the CrewAI Flows are, let us see how to build the CrewAI shopping assistant from the backend to the frontend.

Let’s get started!

Prerequisites

To fully understand this tutorial, you need to have a basic understanding of React or Next.js.

We'll also make use of the following:

  • Python - a popular programming language for building AI agents with LangGraph; make sure it is installed on your computer.

  • OpenAI API - to enable us to perform various tasks using the GPT models; for this tutorial, ensure you have access to the GPT-4 model.

  • CopilotKit - an open-source copilot framework for building custom AI chatbots, in-app AI agents, and text areas.

  • CrewAI - a Python framework that enables developers to create autonomous AI agents with high-level simplicity and precise low-level control.

  • Tavily API - a search engine built specifically for AI agents (LLMs), delivering real-time, accurate, and factual results at speed.

Setting up the project

To get started, clone the CrewAI-Shopping-Assistant repo that consists of a Python-based backend (agent) and a Next.js frontend (frontend).

Next, navigate to the backend directory:

cd agent
Enter fullscreen mode Exit fullscreen mode

Then install the dependencies using Poetry:

poetry install
Enter fullscreen mode Exit fullscreen mode

After that, create a .env file with OpenAI API Key and Tavily API Key:

OPENAI_API_KEY=<<your-OpenAI-key-here>>
TAVILY_API_KEY=<<your-tavily-key-here>>
Enter fullscreen mode Exit fullscreen mode

Then run the agent using the command below:

poetry run python main.py
Enter fullscreen mode Exit fullscreen mode

After that, navigate to the frontend directory:

cd frontend
Enter fullscreen mode Exit fullscreen mode

Next, create a .env file with OpenAI API Key API key:

OPENAI_API_KEY=<<your-OpenAI-key-here>>
Enter fullscreen mode Exit fullscreen mode

Then install the dependencies:

pnpm install
Enter fullscreen mode Exit fullscreen mode

After that, start the development server:

pnpm run dev
Enter fullscreen mode Exit fullscreen mode

Navigate to http://localhost:3000, and you should see the AI shopping assistant UI up and running.

Image from Notion

Let’s now see how to build the AI shopping assistant backend using CrewAI, Tavily, and CopilotKit.

Building the AI shopping assistant backend using CrewAI, Tavily, and CopilotKit

In this section, you will learn how to build the AI shopping assistant backed using CrewAI for agent flows, Tavily for reliable web search, and CopilotKit for state management + UI updates.

Let’s jump in.

Step 1: Define the agent state

First, define the AgentState class that extends CopilotKitState in order to track the product data, user preferences, logs for UI updates, and more, as shown in the agent/shopping_assistant.py file.

from copilotkit.crewai import CopilotKitState
from typing import List, Dict, Any

class AgentState(CopilotKitState):
    """
    Manages the complete state of the shopping workflow.
    """
    # List of products currently displayed on the canvas
    products: List = []

    # List of users' favorite/saved products
    favorites: List = []

    # Temporary buffer to store products before confirmation
    buffer_products: List = []

    # User's wishlist of products
    wishlist: List = []

    # Activity logs to show processing status to the user
    logs: List = []

    # Generated comparison report for products
    report: Any | None = None

    # Flag to control when to show results in UI
    show_results: bool = False

    # Canvas logging information with title and subtitle for UI updates
    canvas_logs: dict = { "title" : "", "subtitle" : "" }
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the Shopping Agent Workflow

Once you have defined the agent state, define the ShoppingAgentFlow class that extends the Flow[AgentState]. Use the start() method to handle the entire flow, where it initializes state, validates environment variables, processes user queries, and emits state updates via CopilotKit, as shown in the ./agent/shopping_assistant.py file.

class ShoppingAgentFlow(Flow[AgentState]):

    @start()
    async def start(self):
        """
        Main entry point for the shopping assistant workflow.
        This method handles the entire flow from user request to product results.
        """
        try:
            # Step 1: Initialize the workflow
            print("Starting Shopping Agent Flow")

            # Update canvas with initial status
            self.state.canvas_logs = {
                "title" : f"Parsing your request",
                "subtitle" : "Deciding to run product search or not"
            }
            await copilotkit_emit_state(self.state)
            await asyncio.sleep(0)

            # Step 2: Validate required environment variables
            if not os.getenv("TAVILY_API_KEY"):
                raise RuntimeError("Missing TAVILY_API_KEY")
            if not os.getenv("OPENAI_API_KEY"):
                raise RuntimeError("Missing OPENAI_API_KEY")

            # Step 3: Check if this is a report generation request (assistant message)
            if self.state.messages[-1]['role'] == 'assistant':
                # Generate and return product comparison report
                result =await generate_report(self.state.products)
                print(result, "result")
                self.state.report = json.loads(result)
                await copilotkit_emit_state(self.state)
                return

            # Step 4: Add initial processing log
            self.state.logs.append({
                "message" : "Analyzing user query",
                "status" : "processing"
            })
            await copilotkit_emit_state(self.state)

            # Step 5: Mark analysis as completed
            self.state.logs[-1]["status"] = "completed"
            await copilotkit_emit_state(self.state)

            // ...
Enter fullscreen mode Exit fullscreen mode

Step 3: Multi-Retailer Product Search with Tavily API

After creating the shopping agent flow, initialize the Tavily client and prepare for multi-retailer search, as shown below.

class ShoppingAgentFlow(Flow[AgentState]):

    @start()
    async def start(self):
        """
        Main entry point for the shopping assistant workflow.
        This method handles the entire flow from user request to product results.
        """
        try:
            // ...

            # Step 13: Set up Tavily search client and result containers
            tv = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
            results_all: List[Dict[str, Any]] = []
            total_mappings_list = []

            # Step 14: Update user with search progress
            self.state.logs.append({
                "message" : "Identifying the sites to search",
                "status" : "processing"
            })
            self.state.canvas_logs={
                "title" : "Identifying the sites to search",
                "subtitle" : "Tavily search in progress...."
            }
            await copilotkit_emit_state(self.state)
            await asyncio.sleep(1)
            self.state.logs[-1]["status"] = "completed"
            await copilotkit_emit_state(self.state)

            // ...
Enter fullscreen mode Exit fullscreen mode

Then search each major retailer using Tavily's domain-specific search capabilities to get relevant product URLs from each major retailer, as shown below.

class ShoppingAgentFlow(Flow[AgentState]):

    @start()
    async def start(self):
        """
        Main entry point for the shopping assistant workflow.
        This method handles the entire flow from user request to product results.
        """
        try:
            // ...

            # Step 15: Search across major retailers for product URLs
                        urls = {}
                        for retailer in RETAILERS:  # RETAILERS = ["target.com", "amazon.com", "ebay.com"]
                            # Search each retailer's domain for products matching the query
                            search = tv.search(
                                query=query,  # User's search query, e.g., "wireless headphones"
                                include_domains=[retailer],  # Limit search to specific retailer
                                include_answer=False,  # Don't include general answers
                                include_images=False,  # Don't include image results
                                include_raw_content=False,  # We'll extract content separately
                                search_depth="advanced",  # Use advanced search for better results
                                max_results=max_search_results,  # Typically 6 results per retailer
                            )

                            # Extract URLs from search results
                            urls[retailer] = [r["url"] for r in search.get("results", []) if r.get("url")]
                            if not urls[retailer]:
                                continue            
            // ...
Enter fullscreen mode Exit fullscreen mode

After gathering URLs, use Tavily's extraction capabilities to get detailed webpage content that includes images and metadata, as shown below.

class ShoppingAgentFlow(Flow[AgentState]):

    @start()
    async def start(self):
        """
        Main entry point for the shopping assistant workflow.
        This method handles the entire flow from user request to product results.
        """
        try:
            // ...

            # Step 17: Define a function for parallel URL content extraction
                        def extract_urls(urls: List[str], retailer: str) -> Dict[str, Any]:
                            """Extract content from URLs for a specific retailer"""
                            try:
                                print(f"Extracting urls for {retailer}. Started at {datetime.now()}")
                                ext1 = tv.extract(urls, extract_depth="advanced", include_images=True, timeout=120)
                                return [ext1, retailer]
                            except Exception as e:
                                print(f"Error extracting urls: {e}")
                                return None

                        # Step 18: Execute parallel extraction for all retailers
                        ext_results = {}
                        with ThreadPoolExecutor(max_workers=3) as executor:
                            # Submit extraction tasks for each retailer
                            futures = {executor.submit(extract_urls, urls[retailer], retailer) : retailer for retailer in RETAILERS}

                            # Collect results as they complete
                            for future in as_completed(futures):
                                result = future.result()
                                ext_results[result[1]] = result[0].get("results", [])
                                if result == None:
                                    print("Condition met! Cancelling remaining tasks...")
                                    # Cancel remaining futures if an error occurs
                                    for f in futures:
                                        f.cancel()
                                    break
            // ...
Enter fullscreen mode Exit fullscreen mode

Finally, process the extracted product data with LLMs to extract product information, as shown below.

class ShoppingAgentFlow(Flow[AgentState]):

    @start()
    async def start(self):
        """
        Main entry point for the shopping assistant workflow.
        This method handles the entire flow from user request to product results.
        """
        try:
            // ...

            # Step 20: Initialize data structures for product processing
            target_listing_pdps: List[str] = []
            done = False
            self.state.logs.append({
                "message" : "Processing the data",
                "status" : "processing"
            })
            await copilotkit_emit_state(self.state)

            # Initialize product containers for each retailer
            products_from_each_site= {
                "target.com" : [],
                "amazon.com" : [],
                "ebay.com" : []
            }

            # Initialize URL replacement counters for each retailer
            retailer_counters = {
                "target.com": {"product": 0, "image": 0},
                "amazon.com": {"product": 0, "image": 0},
                "ebay.com": {"product": 0, "image": 0}
            }

            # Step 21: Define async function for processing extracted data
            async def process_data(ext_results1: Dict[str, Any], retailer: str, retailer_counters: Dict[str, Dict[str, int]]) -> str:
                """Process extracted web content and convert to structured product data"""
                print(f"Processing data for {retailer}. Started at {datetime.now()}")

                for item in ext_results1:
                    url = item["url"]
                    raw = item.get("raw_content") or ""
                    if not raw:
                        return None

                    # Set up URL replacement patterns for each retailer
                    product_base = ""
                    image_base = ""
                    if retailer == "target.com":
                        product_base = "https://tgt.com/url{}"
                        image_base = "https://tgt.com/img/url{}"
                    elif retailer == "amazon.com":
                        product_base = "https://amzn.com/url{}"
                        image_base = "https://amzn.com/img/url{}"
                    elif retailer == "ebay.com":
                        product_base = "https://ebay.com/url{}"
                        image_base = "https://ebay.com/img/url{}"

                    # Replace URLs with standardized product and image links
                    modiefied_text, mappings_list, updated_product_counter, updated_image_counter = replace_urls_with_product_and_image_links(text= raw, product_base= product_base, image_base=image_base, product_counter=retailer_counters[retailer]["product"], image_counter=retailer_counters[retailer]["image"])

                    # Update counters
                    retailer_counters[retailer]["product"] = updated_product_counter
                    retailer_counters[retailer]["image"] = updated_image_counter
                    total_mappings_list.extend(mappings_list)

                    # Determine retailer and page type
                    dom = retailer_of(url)
                    detail_hint = is_pdp(url)

                    # Get structured data assistance for Target specifically
                    assist = parse_target_structured(modified_text) if "target.com" in dom else None

                    # Build prompt for LLM product extraction
                    prompt = build_llm_prompt(modiefied_text, url, assist=assist, detail_hint=detail_hint)

                    try:
                        # Limit products per retailer to avoid overwhelming results
                        if len(products_from_each_site[retailer]) > 2:
                            break
                        print(f"Calling LLM for {url}")
                        data = await call_llm(prompt)
                        print(f"Completed extracting {url}")
                    except Exception as e:
                        # Skip this page if LLM extraction fails
                        print(f"LLM 1st-pass failed for {url}: {e}")
                        continue

                    # Add metadata to extracted data
                    data.setdefault("source_url", url)
                    data.setdefault("retailer", dom)
                    products_from_each_site[retailer] += data["products"]
                return "Completed"
            // ...
Enter fullscreen mode Exit fullscreen mode

Step 4: Configure Human-in-the-Loop functionality

Once the products have been extracted and processed, present them to the user via a tool call to the frontend, as shown below, where the agent pauses to allow the user to review products and make decisions.

class ShoppingAgentFlow(Flow[AgentState]):

    @start()
    async def start(self):
        """
        Main entry point for the shopping assistant workflow.
        This method handles the entire flow from user request to product results.
        """
        try:
            // ...

             # Step 24: Combine and finalize product results
            results_all = combine_products_from_sites(products_from_each_site)
            print(len(results_all), "results_all here")

            # Add unique IDs to each product
            for item in results_all:
                item["id"] = str(uuid.uuid4())

            # Mark data processing as completed
            self.state.logs[-1]["status"] = "completed"
            await copilotkit_emit_state(self.state)

            # Step 25: Apply URL mappings and prepare final results
            updated_products = apply_url_mappings_to_products(results_all, total_mappings_list)
            print(len(updated_products), "updated_products here")
            self.state.buffer_products = updated_products

            print("HERE")
            # Generate a descriptive name for this chat session
            chat_name = await generate_name_for_chat(query)
            print(chat_name, "chat_name here")

            # Step 26: Send product list to user for confirmation
            self.state.messages.append({
                "role" : "assistant",
                "content" : "",
                "tool_calls" : [
                    {
                        "id" : str(uuid.uuid4()),
                        "function" : {
                            "name": "list_products", 
                            "arguments": json.dumps({
                                "products": self.state.buffer_products[:5],  # Show first 5 products
                                "buffer_products" : self.state.buffer_products,  # Keep all products in buffer
                                "chat_name" : chat_name
                            })
                        }
                    }
                ]
            })

            # Step 27: Reset state for user interaction
            self.state.logs = []
            self.state.report = None
            self.state.canvas_logs = {
                "title" : "Awaiting confirmation from the user",
                "subtitle" : "Choose to accept, reject or show all products"
            }
            await copilotkit_emit_state(self.state)
            // ...
Enter fullscreen mode Exit fullscreen mode

After that, when the user responds (via tool calls), handle different scenarios where the user might request more options, accept, or reject the product's recommendations.

class ShoppingAgentFlow(Flow[AgentState]):

    @start()
    async def start(self):
        """
        Main entry point for the shopping assistant workflow.
        This method handles the entire flow from user request to product results.
        """
        try:
            // ...

            # Step 7: Handle tool response messages
                        if(self.state.messages[-1]['role'] == 'tool'):
                            # Handle "Show more products" tool response
                            if(self.state.messages[-1]['content'] == "Show more products"):
                                self.state.messages.append({
                                    "role" : "assistant",
                                    "content" : "Some more products have also been added to be shown in the canvas",
                                    "id" : self.state.messages[-2]['tool_calls'][0]['id']
                                })
                                self.state.logs = []
                                self.state.show_results = True
                                await copilotkit_emit_state(self.state)
                                return self.state

                            # Handle "Rejected" tool response
                            if(self.state.messages[-1]['content'] == "Rejected"):
                                self.state.messages.append({
                                    "role" : "assistant",
                                    "content" : "You have rejected the products. Please try any other product search.",
                                    "id" : self.state.messages[-2]['tool_calls'][0]['id']
                                })
                                self.state.logs = []
                                await copilotkit_emit_state(self.state)
                                return self.state

                            # Handle "Accepted" tool response
                            if(self.state.messages[-1]['content'] == "Accepted"):
                                self.state.messages.append({
                                    "role" : "assistant",
                                    "content" : "The top 5 products have been added to the canvas.",
                                    "id" : self.state.messages[-2]['tool_calls'][0]['id']
                                })
                                self.state.logs = []
                                self.state.show_results = True
                                await copilotkit_emit_state(self.state)
                                return self.state
            // ...
Enter fullscreen mode Exit fullscreen mode

Step 5: Set up a FastAPI endpoint

After creating the ShoppingAgentFlow, set up a FastAPI server that serves as the bridge between our CrewAI shopping agent and the frontend.

To do that, use the CopilotKitRemoteEndpoint to manage the lifecycle of the agent and then use the add_fastapi_endpoint function to mount the CopilotKit's routes to our FastAPI application, as shown in the agent/main.py file.

# Step 1: Import required dependencies
from fastapi import FastAPI  # FastAPI framework for building the API server
import uvicorn  # ASGI server for running FastAPI applications

# Step 2: Import CopilotKit components for agent integration
from copilotkit.integrations.fastapi import add_fastapi_endpoint  # FastAPI integration
from copilotkit import CopilotKitRemoteEndpoint  # Remote endpoint for agent hosting
from copilotkit.crewai.crewai_agent import CrewAIAgent  # CrewAI agent wrapper

# Step 3: Import system dependencies
import os  # Operating system interface for environment variables

# Step 4: Import the shopping assistant flow
from shopping_assistant import ShoppingAgentFlow  # Custom shopping agent implementation

# Step 5: Initialize FastAPI application
app = FastAPI()

# Step 6: Configure CopilotKit Remote Endpoint with Shopping Agent
# This creates a remote endpoint that hosts the shopping assistant agent
sdk = CopilotKitRemoteEndpoint(
    agents=[
        # Step 7: Create CrewAI agent wrapper for the shopping flow
        CrewAIAgent(
            name="shopping_agent_crewai",  # Unique identifier for the agent
            description="A shopping agent that can help you find the best products for your needs by searching various retailers",  # Agent description for CopilotKit
            flow=ShoppingAgentFlow()  # The actual shopping assistant workflow implementation
        )
    ]
)

# Step 8: Add CopilotKit endpoint to FastAPI application
# This mounts the CopilotKit SDK at the "/copilotkit" path
add_fastapi_endpoint(app, sdk, "/copilotkit")

# Step 10: Main server configuration and startup function
def main():
    """
    Configure and run the uvicorn server with development-friendly settings.

    """
    # Step 11: Get server port from environment variable with fallback
    port = int(os.getenv("PORT", "8000"))  # Default to port 8000 if PORT not set

    # Step 12: Start uvicorn server with comprehensive configuration
    uvicorn.run(
        "main:app",  # Application module and instance
        host="0.0.0.0",  # Listen on all network interfaces
        port=port,  # Use configured port
        reload=True,  # Enable hot reload for development
        timeout_keep_alive=900,  # 15 minutes = 900 seconds (for long operations)
        timeout_graceful_shutdown=900,  # 15 minutes graceful shutdown
        reload_dirs=(
            # Step 13: Configure reload directories for development
            ["."] +  # Current directory (always included)
            (["../../../sdk-python/copilotkit"]  # CopilotKit SDK directory if exists
             if os.path.exists("../../../sdk-python/copilotkit")
             else []  # Empty list if SDK directory doesn't exist
             )
        )
    )


# Step 14: Entry point for running the server
if __name__ == "__main__":
    # This ensures the server only starts when the script is run directly
    # (not when imported as a module)
    main()
Enter fullscreen mode Exit fullscreen mode

Congratulations! You have built an AI-powered shopping assistant backend using CrewAI for agent flows, Tavily for web search, and CopilotKit for state management.

Building the CrewAI shopping agent frontend using CopilotKit

In this section, you will learn how to build a frontend for the CrewAI shopping agent using CopilotKit.

Let’s get started!

Step 1: Configure the Copilot Runtime instance

To get started, set up the Copilot runtime instance that serves as the bridge between your frontend and the CrewAI shopping agent backend, as shown below in the frontend/app/api/copilotkit/route.ts file.

/**
 * CopilotKit API Route for Next.js App Router
 *
 * This file sets up the CopilotKit runtime endpoint for the AI Shopping Agent frontend.
 * It handles POST requests to integrate with the CopilotKit service.
 */

import {
  CopilotRuntime,
  copilotRuntimeNextJSAppRouterEndpoint,
  OpenAIAdapter,
} from "@copilotkit/runtime";
import { NextRequest } from "next/server";

// Create an OpenAI adapter for the CopilotKit service
const serviceAdapter = new OpenAIAdapter();

// Initialize the CopilotRuntime with remote endpoints
// This connects to the shopping assistant backend
const runtime = new CopilotRuntime({
  remoteEndpoints: [
    {
      url:
        process.env.NEXT_PUBLIC_SHOPPING_AGENT_URL ||
        "http://localhost:8000/copilotkit",
    },
  ],
});

// POST handler for the CopilotKit API endpoint
// This function processes incoming requests and delegates to the CopilotKit runtime
export const POST = async (req: NextRequest) => {
  const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    serviceAdapter,
    endpoint: "/api/copilotkit",
  });

  return handleRequest(req);
};
Enter fullscreen mode Exit fullscreen mode

Step 2: Set up CopilotKit provider

After setting up a Copilot Runtime instance, set up the CopilotKit provider component that manages your ADK agent sessions.

To set up the CopilotKit Provider, the CopilotKit component must wrap the Copilot-aware parts of your application.

For most use cases, it's appropriate to wrap the CopilotKit provider around the entire app, e.g., in your layout.tsx file.

/**
 * Root Layout Component for the AI Shopping Agent
 *
 * This is the main layout component for the Next.js application.
 * It sets up the CopilotKit provider and global styles for the entire app.
 */

import type { Metadata } from "next";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import "./globals.css";
import "@copilotkit/react-ui/styles.css";
import { CopilotKit } from "@copilotkit/react-core";

// Metadata configuration for the application
export const metadata: Metadata = {
  title: "Shopping Assistant",
  description: "Powered by LangGraph and CopilotKit",
  generator: "v0.app",
};

/**
 * Root Layout Component
 *
 * This component wraps the entire application and provides:
 * - CopilotKit context for AI-powered features
 * - Global font configuration
 * - Base HTML structure
 */
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <head>
        {/* Custom font configuration using Geist fonts */}
        <style>{`
html {
  font-family: ${GeistSans.style.fontFamily};
  --font-sans: ${GeistSans.variable};
  --font-mono: ${GeistMono.variable};
}
        `}</style>
      </head>
      <body>
        {/* CopilotKit Provider - enables AI chat and agent functionality */}
        <CopilotKit
          runtimeUrl="/api/copilotkit" // Points to our CopilotKit API route
          showDevConsole={false} // Hide development console in production
          agent="shopping_agent_crewai" // Specify which backend agent to use
        >
          {children}
        </CopilotKit>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Set up a Copilot chat component

CopilotKit ships with several built-in chat components, which include CopilotPopupCopilotSidebar, and CopilotChat.

To set up a Copilot chat component, define it as shown in the frontend/components/sidebar.tsx file.

/**
 * Sidebar Component for AI Shopping Assistant
 *
 * This component provides the main navigation and chat interface for the shopping assistant.
 * It includes chat session management, view switching, and integrates with CopilotKit for AI interactions.
 */

"use client";

import type React from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { CopilotChat } from "@copilotkit/react-ui";
import { useCopilotChat } from "@copilotkit/react-core";

// ...

/**
 * Main Sidebar Component
 *
 * Features:
 * - Chat session management (create, switch, rename, delete)
 * - View switching between products, wishlist, and reports
 * - Integrated CopilotChat for AI interactions
 * - Product comparison analysis generation
 */
export function Sidebar({
  setQuery,
  isLoading,
  clearState,
  onSearch,
  suggestions,
  currentQuery,
  isSearching,
  currentView,
  wishlistCount,
  goToProducts,
  currentChatId,
  chatSessions,
  onSwitchChat,
  onCreateNewChat,
  onRenameChat,
  onDeleteChat,
}: SidebarProps) {

  // ...

  return (
    <div className="flex flex-col min-h-screen w-80 bg-[#FAFCFA] border-r border-[#D8D8E5]">
      {/* Header */}
      <div className="p-4 border-b border-[#D8D8E5] bg-white">
        <div className="flex items-center gap-3 mb-3">
          <div className="text-2xl">🪁</div>
          <div className="flex-1">
            <h1 className="text-lg font-semibold text-[#030507] font-['Roobert']">
              Shopping Assistant
            </h1>
            <Badge
              variant="secondary"
              className="text-xs bg-[#BEC9FF] text-[#030507] font-semibold">
              PRO
            </Badge>
          </div>
        </div>

                // ...

       <div className="flex-1 overflow-auto">
        <CopilotChat
          className="h-full"
          labels={{
            initial:
              "Hi! I'm your AI shopping assistant. I can help you find and compare products across multiple websites. What are you looking for today? Laptops, Phones, Headphones, etc.",
          }}
        />
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Sync the CrewAI shopping agent state with the frontend using CopilotKit hooks

In CopilotKit, CoAgents maintain a shared state that seamlessly connects your frontend UI with the agent's execution. This shared state system allows you to:

  • Display the agent's current progress and intermediate results

  • Update the agent's state through UI interactions

  • React to state changes in real-time across your application

You can learn more about CoAgents’ shared state here on the CopilotKit docs.

Image from Notion

To sync your CrewAI shopping agent state with the frontend, use the CopilotKit useCoAgent hook, as shown in the frontend/components/shopping-assistant.tsx file.

"use client"

import { useCoAgent } from "@copilotkit/react-core"

export function ShoppingAssistant() {

  // ...

  const { state, setState, start, run } = useCoAgent({
    name: "shopping_agent_crewai",
    initialState: conversationHistory.length > 0 ? { ...conversationHistory[0]?.state, show_results: (conversationHistory[0]?.state?.products?.length > 0 ? true : false) } : {
      products: [],
      favorites: typeof window !== 'undefined' && window.localStorage.getItem("wishlist") ? JSON.parse(window.localStorage.getItem("wishlist") || "[]") : [],
      wishlist: typeof window !== 'undefined' && window.localStorage.getItem("wishlist") ? JSON.parse(window.localStorage.getItem("wishlist") || "[]") : [],
      buffer_products: [],
      logs: [] as ToolLog[],
      report: null,
      show_results: false,
      canvas_logs: {
        title: "",
        subtitle: ""
      }
    }
  })


  return (
    <div className="flex h-screen bg-[#FAFCFA] overflow-hidden">

      // ...

      <div className="flex-1 flex flex-col min-w-0">
        {currentView === "report" ? (
          <ReportView isLoading={isLoading} products={state?.products} onExit={exitToProducts} searchQuery={query} report={state?.report} />
        ) : currentView === "wishlist" ? (

          // ...

        ) : (

          // ...

        )}
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Then render the CrewAI shopping agent's state in the chat UI, which is useful for informing the user about the agent's state in a more in-context way.

To render the CrewAI shopping agent's state in the chat UI, you can use the useCoAgentStateRender hook in the frontend/components/shopping-assistant.tsx file.

"use client"

import { useCoAgentStateRender} from "@copilotkit/react-core"
import { ToolLog, ToolLogs } from "./tool-logs"

export function ShoppingAssistant() {

  // ...

  useCoAgentStateRender({
    name: "shopping_agent_crewai",
    render: (state1: any) => {
      // useEffect(() => {
      // console.log(state1, "state1")
      // }, [state1])

      return <ToolLogs logs={state1?.state?.logs || []} />
    }
  })  

  return (
    <div className="flex h-screen bg-[#FAFCFA] overflow-hidden">

      // ...

      <div className="flex-1 flex flex-col min-w-0">
        {currentView === "report" ? (
          <ReportView isLoading={isLoading} products={state?.products} onExit={exitToProducts} searchQuery={query} report={state?.report} />
        ) : currentView === "wishlist" ? (

          // ...

        ) : (

          // ...

        )}
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

If you execute a query in the chat, you should see the CrewAI shopping agent’s state task execution rendered in the chat UI, as shown below.

Image from Notion

Step 5: Implementing Human-in-the-Loop (HITL) in the frontend

Human-in-the-loop (HITL) allows agents to request human input or approval during execution, making AI systems more reliable and trustworthy. This pattern is essential when building AI applications that need to handle complex decisions or actions that require human judgment.

You can learn more about Human in the Loop here on CopilotKit docs.

Image from Notion

To implement Human-in-the-Loop (HITL) in the frontend, you need to use the CopilotKit useCopilotKitAction hook with the renderAndWaitForResponse method, which allows returning values asynchronously from the render function, as shown in the frontend/components/shopping-assistant.tsx file.

"use client"

import { useCopilotAction } from "@copilotkit/react-core"
import DialogBox from "./tool-response"

export function ShoppingAssistant() {

  // ...

  useCopilotAction({
    name: "list_products",
    description: "A list of products that are scraped from the web",
    renderAndWaitForResponse: ({ status, respond, args }) => {
      // console.log(args, "argsargsargsargs")

      return <DialogBox isDisabled={respond == undefined} contentList={args?.products?.map((product: any) => ({ title: product.title, url: product.product_url }))}
        onAccept={() => {
          debugger
          if (respond) {
            respond("Accepted")
            setState({
              ...state,
              products: args?.products,
              buffer_products: args?.buffer_products.slice(5, args?.buffer_products.length),
              logs: []
            })
            let conversations = conversationHistory
            conversations.forEach((conversation: any) => {
              if (conversation.conversationId == currentChatId) {
                conversation.chatName = args?.chat_name
              }
            })
            setConversationHistory(conversations)
            setCurrentChatId(currentChatId)
            console.log(currentChatId, "currentChatId");
            // setConversationHistory((prev: any) => prev.map((conversation: any) => conversation.conversationId === currentChatId ? { ...conversation, chatName: args?.chat_name } : conversation))
            // setProducts(args?.products)
          }
        }}
        onReject={() => {
          if (respond) {
            respond("Rejected")
            setState({
              ...state,
              logs: []
            })
          }
        }}
        onNeedInfo={() => {
          debugger
          if (respond) {
            respond("Show more products")
            setState({
              ...state,
              products: args?.buffer_products?.slice(0, 10),
              buffer_products: args?.buffer_products.slice(10, args?.buffer_products.length),
              logs: []
            })

            let conversations = conversationHistory
            conversations.forEach((conversation: any) => {
              if (conversation.conversationId === currentChatId) {
                conversation.chatName = args?.chat_name
              }
            })
            console.log(currentChatId, "currentChatId");

            setConversationHistory(conversations)
            setCurrentChatId(currentChatId)
            // setConversationHistory((prev: any) => prev.map((conversation: any) => conversation.conversationId === currentChatId ? { ...conversation, chatName: args?.chat_name } : conversation))
            // setProducts(args?.buffer_products?.slice(0, 10))
          }
        }} />
    }

  })  

  return (
    <div className="flex h-screen bg-[#FAFCFA] overflow-hidden">

      // ...

      <div className="flex-1 flex flex-col min-w-0">
        {currentView === "report" ? (
          <ReportView isLoading={isLoading} products={state?.products} onExit={exitToProducts} searchQuery={query} report={state?.report} />
        ) : currentView === "wishlist" ? (

          // ...

        ) : (

          // ...

        )}
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

When an agent triggers frontend actions by tool/action name to request human input or feedback during execution, the end-user is prompted with a choice (rendered inside the chat UI). Then the user can choose by pressing a button in the chat UI, as shown below.

Image from Notion

Step 6: Streaming AG-UI + CrewAI agent responses in the frontend

To stream your CrewAI shopping agent responses or results in the frontend, pass the agent’s state field values to the frontend components, as shown in the frontend/components/shopping-assistant.tsx file.

"use client"

import { useCoAgent } from "@copilotkit/react-core"

export function ShoppingAssistant() {

  // ...

  const { state, setState, start, run } = useCoAgent({
    name: "shopping_agent_crewai",
    initialState: conversationHistory.length > 0 ? { ...conversationHistory[0]?.state, show_results: (conversationHistory[0]?.state?.products?.length > 0 ? true : false) } : {
      products: [],
      favorites: typeof window !== 'undefined' && window.localStorage.getItem("wishlist") ? JSON.parse(window.localStorage.getItem("wishlist") || "[]") : [],
      wishlist: typeof window !== 'undefined' && window.localStorage.getItem("wishlist") ? JSON.parse(window.localStorage.getItem("wishlist") || "[]") : [],
      buffer_products: [],
      logs: [] as ToolLog[],
      report: null,
      show_results: false,
      canvas_logs: {
        title: "",
        subtitle: ""
      }
    }
  })


  return (
    <div className="flex h-screen bg-[#FAFCFA] overflow-hidden">

      // ...

      <div className="flex-1 flex flex-col min-w-0">
        {currentView === "report" ? (
          <ReportView isLoading={isLoading} products={state?.products} onExit={exitToProducts} searchQuery={query} report={state?.report} />
        ) : currentView === "wishlist" ? (

          <WishlistView
            clearAllWishlist={() => {
              debugger
              setState({
                ...state,
                favorites: []
              })
              setConversationHistory((prev: any) => prev.map((conversation: any) => conversation.conversationId === currentChatId ? { ...conversation, state: { ...conversation.state, favorites: [] } } : conversation))
              if (typeof window !== 'undefined') {
                window.localStorage.setItem("wishlist", JSON.stringify([]))
              }
            }}
            products={state?.favorites}
            onExit={exitToProducts}
            onToggleWishlist={toggleWishlist}
            onDeleteProduct={deleteProduct}
          />

        ) : (
          <Canvas
            canvasLogs={state?.canvas_logs}
            start={run}
            show_results={state?.show_results}
            report={state?.report}
            products={state?.products}
            isLoading={isLoading && !state?.show_results}
            query={query}
            wishlistLength={state?.favorites?.length}
            wishlist={state?.favorites}
            onToggleWishlist={toggleWishlist}
            onDeleteProduct={deleteProduct}
            onGoToWishlist={goToWishlist}
            onGoToReport={goToReport}
          />          
        )}
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

If you query your agent and approve its feedback request, you should see the agent’s response or results streaming in the UI, as shown below.

Conclusion

In this guide, we have walked through the steps of building a full-stack shopping assistant using CrewAI and then adding a frontend to the agents using CopilotKit.

While we’ve explored a couple of features, we have barely scratched the surface of the countless use cases for CopilotKit, ranging from building interactive AI chatbots to building agentic solutions—in essence, CopilotKit lets you add a ton of useful AI capabilities to your products in minutes.

Hopefully, this guide makes it easier for you to integrate AI-powered Copilots into your existing application.

Follow CopilotKit on Twitter and say hi, and if you'd like to build something cool, join the Discord community.

Top comments (5)

Collapse
 
roshan_sharma_7deae5e0742 profile image
roshan sharma

That’s dope! Building a full-stack AI shopping assistant with CrewAI and Tavily sounds super practical and cutting-edge. Nice work putting that together!

Collapse
 
james0123 profile image
James

Thanks for the tutorial Bonnie

Collapse
 
eli_discovers profile image
Eli Berman CopilotKit

This is really cool.
Basically a blueprint for any agent that needs to query multiple sources, normalize results, and present them in a usable way...
Nice breakdown Bonnie

Collapse
 
morgan-123 profile image
Morgan

true!

Collapse
 
mathew00112 profile image
Mathew

Nice tutorial- gonna try this out soon for sure