From Chatbot to Intelligence Crew: Building a Multi-Agent RAG System for Fashion E-commerce with CrewAI

From Chatbot to Intelligence Crew: Building a Multi-Agent RAG System for Fashion E-commerce with CrewAI
Photo by Steve Johnson / Unsplash

The single-agent RAG chatbot we built previously is powerful for answering product queries — but fashion e-commerce decision-makers need more than a smart search box.

An E-commerce Manager needs inventory-aware recommendations, a CTO needs a scalable, observable architecture, and a Social Media Manager needs trend signals translated into content briefs. A single agent can't wear all these hats simultaneously.

This is where CrewAI changes the game: instead of one AI doing everything, you deploy a crew of specialized agents that collaborate, delegate, and synthesize — each optimized for a distinct role in the fashion business pipeline.


Why Move Beyond a Single RAG Agent

Traditional single-agent RAG hits a ceiling on complex, multi-step reasoning. Consider a real-world fashion scenario: a user asks "What should we push on Instagram this week based on trending styles and what we actually have in stock?" — that single question requires:

  1. Trend intelligence retrieval (social listening)
  2. Inventory lookup (vector DB)
  3. Cross-referencing availability vs. trend match
  4. Content brief generation in a specific brand voice

No single prompt-chain handles all four steps reliably. A multi-agent system assigns each layer to a specialist, dramatically improving accuracy and enabling parallel processing of independent subtasks.


The Architecture: A Fashion Intelligence Crew

The system is built around four specialized CrewAI agents, orchestrated in a hybrid sequential-parallel pattern:

AgentRoleTools
Trend ScoutMonitors social signals, runway data, search trendsTavily search API, web scraper
Catalog AnalystQueries Qdrant for inventory semanticsQdrant vector store, metadata filter
Merchandising StrategistCross-references trends vs. stock, scores opportunitiesLLM reasoning, product ranker
Content ArchitectGenerates channel-specific briefs (IG, email, PDP copy)LLM, template engine

This crew serves all three decision-maker personas simultaneously: the E-commerce Manager gets a scored product opportunity list, the CTO gets a modular/observable pipeline, and the Social Media Manager gets ready-to-use content briefs.


Environment Setup

Install dependencies and configure your environment:

  pip install crewai crewai-tools qdrant-client openai tavily-python python-dotenv fastapi uvicorn
python# .env
OPENAI_API_KEY=sk-...
TAVILY_API_KEY=tvly-...
QDRANT_URL=https://your-cluster.qdrant.io
QDRANT_API_KEY=your-qdrant-key
QDRANT_COLLECTION=fashion_catalog


Phase 1 — Define Your Tools

Each agent needs tools it can call autonomously. CrewAI tools are Python functions decorated with @tool.

  # tools/fashion_tools.py
from crewai.tools import tool
from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchAny
from openai import OpenAI
from tavily import TavilyClient
import os

openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
tavily_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
qdrant_client = QdrantClient(
    url=os.getenv("QDRANT_URL"),
    api_key=os.getenv("QDRANT_API_KEY")
)

def get_embedding(text: str) -> list[float]:
    """Generate a 1536-dim embedding using text-embedding-3-small."""
    response = openai_client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return response.data[0].embedding


@tool("Trend Intelligence Search")
def search_fashion_trends(query: str) -> str:
    """
    Search for current fashion trends, runway reports, and social media signals.
    Use this to understand what styles, colors, and categories are trending now.
    Input: a descriptive trend query (e.g., 'spring 2026 color trends womenswear').
    """
    results = tavily_client.search(
        query=query,
        search_depth="advanced",
        max_results=8,
        include_domains=[
            "vogue.com", "wwd.com", "hypebeast.com",
            "fashionunited.com", "stylebop.com"
        ]
    )
    formatted = []
    for r in results.get("results", []):
        formatted.append(f"Source: {r['url']}\nTitle: {r['title']}\nContent: {r['content'][:400]}")
    return "\n\n---\n\n".join(formatted)


@tool("Product Catalog Search")
def search_product_catalog(
    query: str,
    category: str = None,
    sizes: list[str] = None,
    max_price: float = None,
    in_stock_only: bool = True,
    top_k: int = 10
) -> str:
    """
    Search the fashion product catalog using semantic search + metadata filtering.
    Returns matching products with name, price, available sizes, and brand.
    Input: natural language product query plus optional filters.
    """
    query_vector = get_embedding(query)

    # Build Qdrant payload filters
    must_conditions = []
    if in_stock_only:
        must_conditions.append(
            FieldCondition(key="metadata.in_stock", match=MatchAny(any=[True]))
        )
    if category:
        must_conditions.append(
            FieldCondition(key="metadata.category", match=MatchAny(any=[category]))
        )
    if sizes:
        must_conditions.append(
            FieldCondition(key="metadata.sizes", match=MatchAny(any=sizes))
        )

    search_filter = Filter(must=must_conditions) if must_conditions else None

    results = qdrant_client.search(
        collection_name=os.getenv("QDRANT_COLLECTION"),
        query_vector=query_vector,
        query_filter=search_filter,
        limit=top_k,
        with_payload=True,
        score_threshold=0.65  # Only return relevant results
    )

    if not results:
        return "No matching products found in the catalog for this query."

    output = []
    for r in results:
        p = r.payload.get("metadata", {})
        output.append(
            f"Product: {p.get('name', 'N/A')}\n"
            f"  Brand: {p.get('brand', 'N/A')}\n"
            f"  Category: {p.get('category', 'N/A')}\n"
            f"  Price: €{p.get('price', 'N/A')}\n"
            f"  Sizes: {', '.join(p.get('sizes', []))}\n"
            f"  Colors: {', '.join(p.get('colors', []))}\n"
            f"  Relevance Score: {r.score:.2f}\n"
            f"  In Stock: {'✓' if p.get('in_stock') else '✗'}"
        )
    return "\n\n".join(output)


@tool("Content Brief Generator")
def generate_content_brief(
    product_list: str,
    channel: str,
    brand_voice: str,
    trend_context: str
) -> str:
    """
    Generate a channel-specific content brief (Instagram, email, PDP copy).
    Input: product list string, channel name, brand voice description, trend context.
    """
    prompt = f"""You are a senior fashion copywriter.
    
BRAND VOICE: {brand_voice}
CHANNEL: {channel}
TREND CONTEXT: {trend_context}

FEATURED PRODUCTS:
{product_list}

Generate a complete content brief for {channel} including:
1. Hook/headline (2 options)
2. Body copy (channel-appropriate length)
3. Call to action
4. Hashtags (if Instagram)
5. Styling tip that connects the product to the trend

Keep the tone aligned with the brand voice. Be specific, not generic."""

    response = openai_client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.7,
        max_tokens=800
    )
    return response.choices[0].message.content

Phase 2 — Define the Agents

Each agent has a role, goal, backstory, and access to specific tools. The backstory is not decorative — it shapes how the LLM reasons within that agent context.

  # agents/fashion_crew_agents.py
from crewai import Agent, LLM
from tools.fashion_tools import (
    search_fashion_trends,
    search_product_catalog,
    generate_content_brief
)

# Shared LLM configuration
gpt4o = LLM(model="gpt-4o", temperature=0.2)
gpt4o_creative = LLM(model="gpt-4o", temperature=0.75)


trend_scout = Agent(
    role="Fashion Trend Intelligence Analyst",
    goal=(
        "Identify the top 5 actionable fashion trends from social media, "
        "runway coverage, and search data relevant to the current season. "
        "Focus on trends with immediate commercial opportunity."
    ),
    backstory=(
        "You are a former Vogue market editor turned data-driven trend analyst. "
        "You combine editorial intuition with quantitative signal analysis. "
        "You know the difference between a micro-trend and a passing moment, "
        "and you always back trend claims with source evidence."
    ),
    tools=[search_fashion_trends],
    llm=gpt4o,
    verbose=True,
    memory=True,
    max_iter=4
)


catalog_analyst = Agent(
    role="Fashion Catalog & Inventory Intelligence Specialist",
    goal=(
        "Search the product catalog to find items that match trend signals. "
        "Prioritize in-stock items with broad size availability and high relevance scores. "
        "Return structured product data including availability and pricing."
    ),
    backstory=(
        "You are a data-savvy merchandising analyst with deep knowledge of "
        "fashion categories, sizing, and inventory dynamics. "
        "You think like a buyer: availability, margin, and velocity all matter. "
        "You never recommend products that are out of stock or size-limited."
    ),
    tools=[search_product_catalog],
    llm=gpt4o,
    verbose=True,
    memory=True,
    max_iter=5
)


merchandising_strategist = Agent(
    role="Senior Merchandising Strategist",
    goal=(
        "Cross-reference trend intelligence with available inventory. "
        "Score each product-trend pairing by commercial opportunity. "
        "Produce a ranked shortlist of 5-8 products to push this week, "
        "with justification for each choice."
    ),
    backstory=(
        "You are a former head of merchandising at a mid-market fashion retailer. "
        "You have launched over 200 successful campaigns and have a sixth sense for "
        "what will convert. You always think about sell-through rate, margin, and "
        "customer LTV — not just what looks good editorially."
    ),
    tools=[],  # This agent reasons on outputs from prior agents
    llm=gpt4o,
    verbose=True,
    memory=True,
    max_iter=3
)


content_architect = Agent(
    role="Omnichannel Fashion Content Strategist",
    goal=(
        "Transform the merchandising shortlist into ready-to-publish content briefs "
        "for Instagram, email newsletter, and product page (PDP) copy. "
        "Each brief must align with brand voice and embed the trend narrative."
    ),
    backstory=(
        "You are a creative director who has worked for Net-a-Porter, SSENSE, and Zara online. "
        "You understand that great fashion content is equal parts editorial story and conversion trigger. "
        "You write copy that makes people feel something and click something."
    ),
    tools=[generate_content_brief],
    llm=gpt4o_creative,
    verbose=True,
    memory=True,
    max_iter=3
)

Phase 3 — Define Tasks and Assemble the Crew

Tasks connect agents to specific objectives with defined inputs and expected outputs:

  # crew/fashion_intelligence_crew.py
from crewai import Crew, Task, Process
from agents.fashion_crew_agents import (
    trend_scout, catalog_analyst,
    merchandising_strategist, content_architect
)

def build_fashion_crew(
    season: str = "Spring 2026",
    category: str = "womenswear",
    brand_voice: str = "modern minimalist, sustainability-forward, aspirational but accessible",
    target_channels: list[str] = ["Instagram", "Email Newsletter", "PDP"]
) -> Crew:

    # Task 1: Trend research (runs first)
    task_trends = Task(
        description=(
            f"Research the top 5 fashion trends for {season} in {category}. "
            f"For each trend, provide: trend name, key visual/style signals, "
            f"relevant search terms, and one concrete example from a major fashion source. "
            f"Focus on trends with immediate retail relevance, not runway fantasy."
        ),
        expected_output=(
            "A structured report of 5 trends with: trend name, description (2-3 sentences), "
            "key product categories affected, suggested search queries for catalog lookup."
        ),
        agent=trend_scout
    )

    # Task 2: Catalog search (runs in parallel with or after trends)
    task_catalog = Task(
        description=(
            f"Using the trend intelligence from the previous task, search the product catalog "
            f"for items that map to each identified trend. For each trend, find 3-5 matching "
            f"in-stock products. Filter by: in_stock=True, category={category}. "
            f"Return full product details including sizes, colors, and prices."
        ),
        expected_output=(
            "A catalog match report: for each trend, list 3-5 matching products with "
            "name, brand, price, sizes, colors, relevance score, and stock status."
        ),
        agent=catalog_analyst,
        context=[task_trends]  # Receives trend output as context
    )

    # Task 3: Merchandising strategy (sequential, after both above)
    task_strategy = Task(
        description=(
            "Using the trend analysis and catalog matches, create a ranked shortlist of "
            "5-8 products to activate this week. For each product, provide: "
            "opportunity score (1-10), why it fits the trend, expected conversion strength, "
            "and which channels (Instagram/email/PDP) it's best suited for. "
            "Exclude products with fewer than 3 size options available."
        ),
        expected_output=(
            "A ranked product activation plan with: product name, opportunity score, "
            "trend alignment rationale, channel recommendation, and any risk factors."
        ),
        agent=merchandising_strategist,
        context=[task_trends, task_catalog]
    )

    # Task 4: Content generation (final layer)
    task_content = Task(
        description=(
            f"For the top 3 products in the merchandising shortlist, create content briefs for: "
            f"{', '.join(target_channels)}. "
            f"Brand voice: '{brand_voice}'. "
            f"Each brief must weave the trend narrative into the product story. "
            f"For Instagram: include hook, caption, CTA, 10 hashtags. "
            f"For Email: include subject line (2 options), preview text, body copy, CTA. "
            f"For PDP: include headline, bullet benefits (5), and a closing line."
        ),
        expected_output=(
            "Three complete channel content briefs (Instagram, Email, PDP) for each "
            "of the top 3 products. All copy must be brand-voice aligned and trend-connected."
        ),
        agent=content_architect,
        context=[task_strategy]
    )

    crew = Crew(
        agents=[trend_scout, catalog_analyst, merchandising_strategist, content_architect],
        tasks=[task_trends, task_catalog, task_strategy, task_content],
        process=Process.sequential,  # Tasks run in defined order
        verbose=True,
        memory=True,  # Agents share episodic memory across tasks
        planning=True  # Manager-level planning before execution
    )

    return crew

Phase 4 — Run the Crew and Expose via API

This is where the orchestration comes alive. Wrap it in a FastAPI endpoint so it plugs into n8n, your CMS, or any frontend:

  # main.py
from fastapi import FastAPI, BackgroundTasks
from pydantic import BaseModel
from crew.fashion_intelligence_crew import build_fashion_crew
import uuid, json
from datetime import datetime

app = FastAPI(title="Fashion Intelligence Crew API", version="1.0")

# In-memory job store (use Redis in production)
jobs = {}

class CrewRequest(BaseModel):
    season: str = "Spring 2026"
    category: str = "womenswear"
    brand_voice: str = "modern minimalist, sustainability-forward, aspirational but accessible"
    target_channels: list[str] = ["Instagram", "Email Newsletter", "PDP"]

class CrewResponse(BaseModel):
    job_id: str
    status: str
    message: str


def run_crew_job(job_id: str, request: CrewRequest):
    """Runs the crew asynchronously and stores results."""
    try:
        jobs[job_id]["status"] = "running"
        crew = build_fashion_crew(
            season=request.season,
            category=request.category,
            brand_voice=request.brand_voice,
            target_channels=request.target_channels
        )
        result = crew.kickoff()
        jobs[job_id]["status"] = "completed"
        jobs[job_id]["result"] = str(result)
        jobs[job_id]["completed_at"] = datetime.now().isoformat()
    except Exception as e:
        jobs[job_id]["status"] = "failed"
        jobs[job_id]["error"] = str(e)


@app.post("/crew/run", response_model=CrewResponse)
async def run_crew(request: CrewRequest, background_tasks: BackgroundTasks):
    """Launch a Fashion Intelligence Crew job asynchronously."""
    job_id = str(uuid.uuid4())
    jobs[job_id] = {
        "status": "queued",
        "created_at": datetime.now().isoformat(),
        "request": request.model_dump()
    }
    background_tasks.add_task(run_crew_job, job_id, request)
    return CrewResponse(
        job_id=job_id,
        status="queued",
        message=f"Crew activated. Poll /crew/status/{job_id} for results."
    )


@app.get("/crew/status/{job_id}")
async def get_crew_status(job_id: str):
    """Check the status and results of a crew job."""
    if job_id not in jobs:
        return {"error": "Job not found"}
    return jobs[job_id]


@app.get("/crew/health")
async def health():
    return {"status": "ok", "active_jobs": len([j for j in jobs.values() if j["status"] == "running"])}

Run it with:

  uvicorn main:app --host 0.0.0.0 --port 8000 --reload

Phase 5 — Connecting the Crew to n8n

The API above integrates natively into your existing n8n RAG workflow as an HTTP Request node.

Here is the trigger-and-poll pattern:

  [Schedule Trigger: Monday 8AM]
       ↓
[HTTP Request: POST /crew/run] → job_id
       ↓
[Wait: 120s]
       ↓
[HTTP Request: GET /crew/status/{job_id}]
       ↓
[IF: status == "completed"]
       ↓
[Split output by channel]
       ↓
[Notion/Airtable/CMS node: save briefs]
       ↓
[Email node: send digest to team]

This turns the crew output into a weekly automated intelligence brief delivered directly to your team's tools — zero manual work after setup.


What Each Decision-Maker Gets

Every crew run produces three distinct outputs tailored to each persona:ragaboutit+1

  • E-commerce Manager → A ranked product activation plan with opportunity scores, sell-through signals, and channel allocation. No more gut-feel decisions about what to push on homepage.
  • CTO → A fully observable, containerizable Python service with structured logs (verbose=True), async job handling, REST API, and CrewAI's built-in tracing. Each agent run is independently auditable.
  • Social Media Manager → Three complete, ready-to-publish content briefs per top product: Instagram caption + hashtags, email subject + body, and PDP headline + bullets — all in brand voice, all trend-connected.

Production Hardening

Before going live, address these critical points:

  • Agent memory persistence: Replace CrewAI's default in-memory store with a vector-backed memory (Qdrant or ChromaDB) so agents accumulate institutional knowledge across weekly runs
  • Rate limit management: Both OpenAI and Tavily have RPM limits. Use max_iter caps per agent (set to 3–5) and implement exponential backoff in your tool wrappers
  • Crew output validation: Add a Pydantic validation layer after crew.kickoff() to ensure all expected fields (product names, scores, copy) are present before sending to downstream systems
  • Cost estimation: A full crew run with GPT-4o across 4 agents and 4 tasks typically costs $0.08–$0.35 depending on context length — run it weekly, not daily, unless you cache intermediate results
  • Observability: Enable CrewAI's built-in tracing and connect it to LangSmith or a custom logging endpoint so you can audit exactly which agent made which decision

The jump from a single RAG chatbot to a multi-agent intelligence crew is the jump from answering questions to generating strategy — and in fashion e-commerce, that distinction is the difference between a tool and a competitive