Week 3 of 4

πŸ—οΈ Building Real Agents

RAG, Frameworks, Workflows & Multi-Agent Systems

πŸ“… Day 11 – Day 15
⏱️ ~45 min per session
🎯 Use Case: Customer Support Agent
πŸ› οΈ LangChain + LangGraph + Ollama
DAY 11
Frameworks β€” LangChain, Semantic Kernel, CrewAI
⏱️ 45 min πŸ“– Comparison + Code 🎯 Choose the right framework

🎯 Learning Objectives

πŸ—ΊοΈ The Framework Landscape

🦜
LangChain
Python / JS
Most popular, huge ecosystem
Chains, agents, RAG
πŸ”€
LangGraph
Python / JS
Stateful graph workflows
Complex multi-step agents
🧠
Semantic Kernel
C# / Python
Microsoft, .NET native
Enterprise integration
πŸ‘₯
CrewAI
Python
Multi-agent collaboration
Roles, tasks, crews
πŸ’¬
AutoGen
Python
Microsoft, agent conversations
Multi-agent chat

Feature Comparison

FeatureLangChainLangGraphSemantic KernelCrewAI
LanguagePython, JSPython, JSC#, Python, JavaPython
Agent TypeSingle agentGraph workflowsSingle / pluginsMulti-agent crews
RAG Supportβœ… Built-inβœ… Via LangChainβœ… Memory pluginsβœ… Via tools
Tool Callingβœ… @tool decoratorβœ… Node functionsβœ… KernelFunctionβœ… @tool decorator
Memoryβœ… Multiple typesβœ… State graphβœ… Persistentβœ… Short/long term
Local LLM (Ollama)βœ…βœ…βœ…βœ…
Enterprise .NET⚠️ Python only⚠️ Python onlyβœ… C# native⚠️ Python only
Multi-Agent⚠️ Manualβœ… Graph nodes⚠️ Manualβœ… Built-in
Learning Curve🟑 MediumπŸ”΄ Steep🟑 Medium🟒 Easy

🦜 LangChain β€” Hello World Agent

Python β€” LangChain
from langchain_ollama import ChatOllama
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.tools import tool

# Tool
@tool
def lookup_order(order_id: str) -> str:
    """Look up order by ID. Returns status and ETA."""
    return f'{{"order_id": "{order_id}", "status": "shipped", "eta": "2026-04-30"}}'

# LLM + Prompt
llm = ChatOllama(model="qwen2.5:3b", temperature=0.2)
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a customer support agent. Use tools to help customers."),
    MessagesPlaceholder("chat_history", optional=True),
    ("human", "{input}"),
    MessagesPlaceholder("agent_scratchpad"),
])

# Wire up
agent = create_tool_calling_agent(llm, [lookup_order], prompt)
executor = AgentExecutor(agent=agent, tools=[lookup_order], verbose=True)

# Run
result = executor.invoke({"input": "Where is my order #7845?"})
print(result["output"])

🧠 Semantic Kernel β€” Hello World Agent

C# β€” Semantic Kernel
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using System.ComponentModel;

// Define a plugin (tool)
public class OrderPlugin
{
    [KernelFunction, Description("Look up order by ID. Returns status and ETA.")]
    public string LookupOrder([Description("The order ID")] string orderId)
    {
        return $"{{\"order_id\": \"{orderId}\", \"status\": \"shipped\", \"eta\": \"2026-04-30\"}}";
    }
}

// Setup kernel
var builder = Kernel.CreateBuilder();
builder.AddOllamaChatCompletion("qwen2.5:3b", new Uri("http://localhost:11434"));
builder.Plugins.AddFromType<OrderPlugin>();
var kernel = builder.Build();

// Chat with auto function calling
var settings = new PromptExecutionSettings {
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};

var chatService = kernel.GetRequiredService<IChatCompletionService>();
var history = new ChatHistory("You are a customer support agent.");
history.AddUserMessage("Where is my order #7845?");

var response = await chatService.GetChatMessageContentAsync(history, settings, kernel);
Console.WriteLine(response.Content);

πŸ‘₯ CrewAI β€” Hello World Agent

Python β€” CrewAI
from crewai import Agent, Task, Crew
from crewai_tools import tool
from langchain_ollama import ChatOllama

@tool("Order Lookup")
def lookup_order(order_id: str) -> str:
    """Look up order by ID. Returns status and ETA."""
    return f'{{"order_id": "{order_id}", "status": "shipped", "eta": "2026-04-30"}}'

# Define agent with role
support_agent = Agent(
    role="Customer Support Specialist",
    goal="Resolve customer issues quickly and empathetically",
    backstory="You are an experienced support agent for ShopEasy e-commerce.",
    tools=[lookup_order],
    llm=ChatOllama(model="qwen2.5:3b", temperature=0.2),
    verbose=True
)

# Define task
resolve_task = Task(
    description="Customer asks: 'Where is my order #7845?' Resolve this.",
    expected_output="A helpful response with order status",
    agent=support_agent
)

# Create crew and run
crew = Crew(agents=[support_agent], tasks=[resolve_task], verbose=True)
result = crew.kickoff()
print(result)
🎯
Our course path: We use LangChain (Days 11-13) for single agents + RAG, LangGraph (Day 14) for complex workflows, CrewAI (Day 15) for multi-agent, and Semantic Kernel (Week 4) for .NET integration.

🦜 The LangChain Ecosystem

LangChain Family
🦜
LangChain Core
Chains, prompts, agents, tools
The foundation library
πŸ”€
LangGraph
Stateful graph workflows
Complex agent orchestration
πŸ”
LangSmith
Tracing, debugging, evaluation
Production monitoring
πŸš€
LangServe
Deploy chains as REST APIs
Production serving

βœ… Key Takeaways

  • LangChain = most popular, best ecosystem, ideal for Python agents + RAG
  • LangGraph = for complex stateful workflows (branching, loops, human-in-the-loop)
  • Semantic Kernel = best for .NET/C# enterprise integration
  • CrewAI = simplest way to build multi-agent systems
  • All frameworks support Ollama (local LLMs) β€” no vendor lock-in

❓ Quick Check

  1. Which framework would you choose for a C# / .NET backend?
  2. What's the difference between LangChain and LangGraph?
  3. Which framework makes multi-agent collaboration easiest?
  4. What does LangSmith provide?
DAY 12
Environment Setup β€” Ollama, Python, .NET
⏱️ 45 min πŸ’» 100% Hands-On 🎯 Dev environment ready

🎯 Learning Objectives

πŸ“‹ Prerequisites Checklist

ComponentVersionPurpose
Python3.11+Agent development, LangChain, CrewAI
OllamaLatestLocal LLM runtime
.NET SDK8.0+Semantic Kernel (Week 4)
VS CodeLatestIDE with Copilot
GitLatestVersion control

πŸ› οΈ Step 1: Ollama Setup

Terminal
# Install Ollama (Windows)
winget install Ollama.Ollama

# Start Ollama service
ollama serve

# Pull models
ollama pull qwen2.5:3b          # Main LLM (1.9 GB)
ollama pull nomic-embed-text    # Embedding model (274 MB)
ollama pull llama3.1:8b         # Alternative LLM β€” optional (4.7 GB)

# Verify
ollama list
ollama run qwen2.5:3b "Hello, are you working?"

🐍 Step 2: Python Environment

Terminal
# Create project directory
mkdir customer-support-agent
cd customer-support-agent

# Create virtual environment
python -m venv venv
.\venv\Scripts\Activate.ps1     # Windows PowerShell
# source venv/bin/activate      # Mac/Linux

# Install core dependencies
pip install langchain langchain-ollama langchain-core langchain-community
pip install langchain-chroma     # Vector store for RAG
pip install langgraph            # Graph workflows
pip install crewai crewai-tools  # Multi-agent
pip install chromadb             # Vector database
pip install streamlit            # Web UI
pip install python-dotenv        # Environment variables
pip install tiktoken             # Tokenizer utilities

# Verify installation
python -c "from langchain_ollama import ChatOllama; print('LangChain + Ollama ready!')"

Project Structure

Project Layout
customer-support-agent/
β”œβ”€β”€ venv/                      # Virtual environment
β”œβ”€β”€ .env                       # Environment variables
β”œβ”€β”€ requirements.txt           # Python dependencies
β”‚
β”œβ”€β”€ config/
β”‚   └── settings.py            # Model, temperature, prompts
β”‚
β”œβ”€β”€ tools/
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ order_tools.py         # lookup_order, cancel_order
β”‚   β”œβ”€β”€ policy_tools.py        # get_refund_policy, get_faq
β”‚   └── communication_tools.py # send_email, escalate
β”‚
β”œβ”€β”€ knowledge/
β”‚   β”œβ”€β”€ policies.txt           # Company policies
β”‚   β”œβ”€β”€ faqs.txt               # FAQ documents
β”‚   └── products.txt           # Product information
β”‚
β”œβ”€β”€ agents/
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ support_agent.py       # Main ReAct agent
β”‚   β”œβ”€β”€ rag_agent.py           # RAG-powered agent
β”‚   └── crew_agent.py          # Multi-agent crew
β”‚
β”œβ”€β”€ memory/
β”‚   └── chroma_db/             # Persistent vector store
β”‚
β”œβ”€β”€ app_cli.py                 # CLI interface
β”œβ”€β”€ app_streamlit.py           # Streamlit web UI
└── tests/
    └── test_agent.py          # Agent tests

βš™οΈ Step 3: Configuration

config/settings.py
# Agent Configuration
OLLAMA_BASE_URL = "http://localhost:11434"
LLM_MODEL = "qwen2.5:3b"
EMBEDDING_MODEL = "nomic-embed-text"
TEMPERATURE = 0.2
MAX_TOKENS = 1024
MAX_AGENT_ITERATIONS = 5

# System Prompt
SYSTEM_PROMPT = """You are a customer support agent for ShopEasy, India's trusted e-commerce platform.

You have access to tools to look up orders, check policies, and search the knowledge base.

Rules:
1. ALWAYS use tools to verify information β€” never guess
2. Be empathetic, professional, and concise
3. Reference specific order IDs and amounts
4. If you cannot resolve, escalate: "Let me connect you to a specialist"
5. Never fabricate policies, discounts, or order information
6. Keep responses under 150 words
"""

βœ… Step 4: Verify Everything Works

verify_setup.py
"""Run this to verify your entire setup works."""
import sys

def check_ollama():
    import requests
    try:
        r = requests.get("http://localhost:11434/api/tags", timeout=5)
        models = [m["name"] for m in r.json().get("models", [])]
        assert any("qwen" in m for m in models), "qwen2.5:3b not found!"
        print(f"βœ… Ollama running. Models: {models}")
    except Exception as e:
        print(f"❌ Ollama not running: {e}")
        sys.exit(1)

def check_langchain():
    from langchain_ollama import ChatOllama
    llm = ChatOllama(model="qwen2.5:3b", temperature=0)
    response = llm.invoke("Say 'Hello Agent' and nothing else.")
    print(f"βœ… LangChain + Ollama: {response.content[:50]}...")

def check_embeddings():
    from langchain_ollama import OllamaEmbeddings
    emb = OllamaEmbeddings(model="nomic-embed-text")
    vector = emb.embed_query("test embedding")
    print(f"βœ… Embeddings working. Dimension: {len(vector)}")

def check_chromadb():
    import chromadb
    client = chromadb.Client()
    col = client.create_collection("test")
    col.add(documents=["test doc"], ids=["1"])
    results = col.query(query_texts=["test"], n_results=1)
    print(f"βœ… ChromaDB working. Query result: {results['documents']}")

if __name__ == "__main__":
    print("πŸ” Verifying setup...\n")
    check_ollama()
    check_langchain()
    check_embeddings()
    check_chromadb()
    print("\nπŸŽ‰ All checks passed! You're ready to build agents.")

πŸ”§ Step 5: .NET Setup (for Week 4)

Terminal β€” .NET
# Verify .NET SDK
dotnet --version    # Should be 8.0+

# Create project
dotnet new console -n ShopEasyAgent
cd ShopEasyAgent

# Add Semantic Kernel
dotnet add package Microsoft.SemanticKernel
dotnet add package Microsoft.SemanticKernel.Connectors.Ollama --prerelease

# Verify
dotnet build

βœ… Key Takeaways

  • Ollama + qwen2.5:3b + nomic-embed-text = complete local AI stack
  • LangChain ecosystem: langchain, langchain-ollama, langchain-chroma, langgraph
  • Project structure separates: tools, agents, knowledge, config, memory
  • Always verify setup with a test script before building
  • .NET + Semantic Kernel ready for Week 4 integration

πŸ”¨ Hands-On Tasks

  1. Set up the complete project structure as shown above
  2. Run verify_setup.py and ensure all checks pass
  3. Send a test message to Ollama via Python and print the response
  4. Generate an embedding for "I want a refund" and print its dimension
DAY 13
First Agent β€” RAG-Powered Q&A
⏱️ 45 min πŸ’» Full Build 🎯 Working RAG agent

🎯 Learning Objectives

πŸ“– What is RAG?

RAG (Retrieval-Augmented Generation) gives the LLM access to external knowledge at query time. Instead of relying only on training data, the LLM retrieves relevant documents and uses them to generate accurate, grounded answers.

RAG Pipeline β€” End to End
πŸ“¨ Customer
Question
β†’
πŸ“ Embed
Question
β†’
πŸ” Search
Vector Store
β†’
πŸ“„ Top-K
Documents
β†’
🧠 LLM +
Context
β†’
πŸ’¬ Grounded
Answer

Why RAG? β€” LLM Alone vs RAG

❌ LLM Without RAG

Q: "What's ShopEasy's refund policy?"
A: "Most e-commerce companies offer 
    30-day refund windows..."
    ← HALLUCINATED β€” made up generic answer!

βœ… LLM With RAG

Q: "What's ShopEasy's refund policy?"
[Retrieved: policies.txt β†’ "7-day full refund, 
 after 7 days store credit only"]
A: "ShopEasy offers full refund within 7 days.
    After 7 days, you get store credit."
    ← ACCURATE β€” grounded in real data!

πŸ—οΈ Step 1: Prepare Knowledge Base Documents

knowledge/policies.txt
## Refund Policy
- Full refund within 7 days of delivery for all products
- After 7 days: store credit only (valid for 6 months)
- Refund processed within 3-5 business days to original payment method
- Electronics: 15-day return window
- Fashion: 30-day return window
- Damaged items: full refund + β‚Ή500 inconvenience credit (photo required)

## Shipping Policy
- Standard delivery: 5-7 business days (free on orders above β‚Ή999)
- Express delivery: 1-2 business days (β‚Ή149 flat fee)
- Same-day delivery: available in metro cities (β‚Ή299)
- Shipping delay >2 days: β‚Ή200 store credit automatically applied
- Shipping delay >7 days: full refund option available

## Cancellation Policy
- Orders can be cancelled before shipping at no charge
- After shipping: must wait for delivery and initiate return
- Digital products: non-cancellable after activation

## Payment Methods
- UPI (Google Pay, PhonePe, Paytm)
- Credit Cards (Visa, Mastercard, RuPay)
- Debit Cards
- Net Banking
- EMI available on orders above β‚Ή3,000
- Cash on Delivery (β‚Ή49 handling fee)

## Warranty
- All electronics: 1-year manufacturer warranty
- Extended warranty available for β‚Ή999/year
- Warranty does not cover physical damage
- Warranty claims: upload proof of purchase + photos of issue

πŸ—οΈ Step 2: Build the Vector Store

Python β€” Ingest documents into ChromaDB
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma

# 1. Load documents
loader = TextLoader("knowledge/policies.txt", encoding="utf-8")
documents = loader.load()

# 2. Split into chunks
splitter = RecursiveCharacterTextSplitter(
    chunk_size=300,         # Each chunk ~300 chars
    chunk_overlap=50,       # 50 char overlap between chunks
    separators=["\n\n", "\n", ". ", " "]
)
chunks = splitter.split_documents(documents)
print(f"Split into {len(chunks)} chunks")

# 3. Create embeddings and store in ChromaDB
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./memory/chroma_db",
    collection_name="support_kb"
)
print(f"βœ… Vector store created with {vectorstore._collection.count()} documents")

# 4. Test retrieval
results = vectorstore.similarity_search("What is the refund policy?", k=3)
for i, doc in enumerate(results):
    print(f"\n--- Result {i+1} ---")
    print(doc.page_content)

πŸ—οΈ Step 3: Build the RAG Chain

Python β€” RAG Agent
from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

# Load existing vector store
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectorstore = Chroma(
    persist_directory="./memory/chroma_db",
    collection_name="support_kb",
    embedding_function=embeddings
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# LLM
llm = ChatOllama(model="qwen2.5:3b", temperature=0.2)

# RAG Prompt
rag_prompt = ChatPromptTemplate.from_template("""
You are a customer support agent for ShopEasy.
Answer the customer's question using ONLY the context below.
If the answer is not in the context, say: "I don't have that information. Let me connect you to a specialist."

Context:
{context}

Customer Question: {question}

Instructions:
- Be specific β€” quote policies with exact numbers/days
- Be empathetic and professional
- Keep response under 100 words

Your Response:""")

# Helper: format retrieved docs
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

# RAG Chain
rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | rag_prompt
    | llm
    | StrOutputParser()
)

# Test it!
questions = [
    "What's your refund policy for electronics?",
    "Do you offer same-day delivery?",
    "Can I pay with UPI?",
    "I received a damaged item, what do I do?",
    "What's the warranty on laptops?",
]

for q in questions:
    print(f"\n❓ {q}")
    answer = rag_chain.invoke(q)
    print(f"πŸ’¬ {answer}")

πŸ—οΈ Step 4: RAG Agent with Tools + Memory

Python β€” Full RAG Support Agent
from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_chroma import Chroma
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.memory import ConversationBufferWindowMemory
from langchain.tools import tool
import json

# --- Tools ---
@tool
def search_knowledge_base(query: str) -> str:
    """Search ShopEasy's knowledge base for policies, FAQs, and product info.
    Use this when customer asks about policies, procedures, or general info."""
    embeddings = OllamaEmbeddings(model="nomic-embed-text")
    vs = Chroma(persist_directory="./memory/chroma_db",
                collection_name="support_kb", embedding_function=embeddings)
    docs = vs.similarity_search(query, k=3)
    return "\n\n".join(f"[Source {i+1}]: {d.page_content}" for i, d in enumerate(docs))

@tool
def lookup_order(order_id: str) -> str:
    """Look up order by ID. Returns status, items, total, and delivery info."""
    orders = {
        "7845": {"order_id": "7845", "status": "shipped", "eta": "2026-04-30",
                 "items": ["MacBook Air M3"], "total": 89999, "customer": "Rajesh Kumar"},
        "7846": {"order_id": "7846", "status": "delivered", "delivered_on": "2026-04-25",
                 "items": ["iPhone 16"], "total": 79999, "customer": "Priya Singh"},
    }
    order = orders.get(order_id.strip())
    return json.dumps(order) if order else f"Order #{order_id} not found."

@tool
def escalate_to_human(reason: str) -> str:
    """Escalate the conversation to a human agent. Use when you cannot resolve the issue."""
    return f"Ticket created. Reason: {reason}. A specialist will respond within 2 hours."

# --- Agent Setup ---
llm = ChatOllama(model="qwen2.5:3b", temperature=0.2)
tools = [search_knowledge_base, lookup_order, escalate_to_human]
memory = ConversationBufferWindowMemory(memory_key="chat_history", return_messages=True, k=5)

prompt = ChatPromptTemplate.from_messages([
    ("system", """You are a customer support agent for ShopEasy, India's trusted e-commerce platform.

Tools available:
- search_knowledge_base: Search policies, FAQs, product info
- lookup_order: Check order status by order ID
- escalate_to_human: Escalate to human agent

Rules:
1. ALWAYS search the knowledge base before answering policy questions
2. ALWAYS look up orders before making claims about order status
3. Be empathetic, specific, and concise (under 100 words)
4. If unsure, escalate β€” never guess or hallucinate
5. Reference exact policy details (days, amounts) from search results
"""),
    MessagesPlaceholder("chat_history"),
    ("human", "{input}"),
    MessagesPlaceholder("agent_scratchpad"),
])

agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(
    agent=agent, tools=tools, memory=memory,
    verbose=True, max_iterations=5, handle_parsing_errors=True
)

# --- Interactive Loop ---
print("🎧 ShopEasy Support Agent (type 'quit' to exit)\n")
while True:
    user_input = input("You: ").strip()
    if user_input.lower() in ("quit", "exit", "q"):
        break
    result = executor.invoke({"input": user_input})
    print(f"\nπŸ€– Agent: {result['output']}\n")
🎧 Use Case Connection

Customer Support Agent β€” RAG Architecture

Complete RAG Agent Flow
πŸ“¨ Customer: "I got a damaged phone. What's the refund process?"
↓ Agent reasons
🧠 "I need the damage refund policy. Let me search the knowledge base."
↓ Tool call
πŸ” search_knowledge_base("damaged item refund policy")
↓ Retrieved
πŸ“„ "Damaged items: full refund + β‚Ή500 inconvenience credit (photo required)"
↓ Generate response using context
πŸ’¬ "I'm sorry about the damaged phone! For damaged items, you'll receive a full refund plus a β‚Ή500 inconvenience credit. Please upload a photo of the damage and we'll process it within 3-5 business days."

βœ… Key Takeaways

  • RAG = Retrieve relevant docs + Augment the prompt + Generate grounded answer
  • ChromaDB stores document embeddings for fast semantic search
  • Text splitting (chunking) is critical β€” too big = noise, too small = lost context
  • RAG eliminates hallucination by grounding answers in real documents
  • Combine RAG (knowledge search) with tools (order lookup) for a complete agent

πŸ”¨ Hands-On Tasks

  1. Create policies.txt with at least 5 policy sections
  2. Ingest into ChromaDB and verify with 5 test queries
  3. Build the RAG chain and test with policy questions
  4. Build the full agent (RAG + order lookup + escalation) and test interactively
DAY 14
LangGraph β€” Stateful Agent Workflows
⏱️ 45 min πŸ’» Build + Theory 🎯 Graph-based agents

🎯 Learning Objectives

πŸ€” Why LangGraph?

Simple agents (AgentExecutor) work for straightforward tasks. But real support scenarios need complex workflows:

Limitation of AgentExecutorLangGraph Solution
No branching: can't route to different handlersConditional edges β€” route by intent
No human approval: auto-executes everythingInterrupt nodes β€” pause for human
No parallel paths: sequential onlyParallel nodes β€” execute simultaneously
Hard to retry specific stepsCheckpointing β€” save & replay state
One agent does everythingMulti-node β€” different LLMs per step

πŸ“ LangGraph Concepts

β­•
Nodes
Python functions that do work
(classify, retrieve, generate, etc.)
➑️
Edges
Connections between nodes
Normal or conditional
πŸ“¦
State
Shared data dictionary
Passed through the graph

πŸ—οΈ Customer Support Workflow Graph

Support Agent β€” LangGraph Workflow
πŸ“¨ START
Customer message arrives
↓
🏷️ classify_intent
Billing? Shipping? Product? Account?
↓ conditional edge
πŸ’° billing
handle_billing
πŸ“¦ shipping
handle_shipping
πŸ”§ product
handle_product
🚨 escalate
human_review
↓ all paths converge
πŸ“ generate_response
Draft reply using gathered context
↓
βœ… quality_check
Verify response meets guidelines
↓
πŸ’¬ END
Send response to customer

Full Implementation

Python β€” LangGraph Support Workflow
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langchain_ollama import ChatOllama

llm = ChatOllama(model="qwen2.5:3b", temperature=0.2)

# --- State Definition ---
class SupportState(TypedDict):
    customer_message: str
    intent: str
    context: str        # Retrieved info from tools
    sentiment: str
    draft_response: str
    final_response: str

# --- Node Functions ---
def classify_intent(state: SupportState) -> SupportState:
    """Classify the customer's intent."""
    msg = state["customer_message"]
    response = llm.invoke(
        f"Classify this customer message into exactly one category: "
        f"billing, shipping, product, account, escalate.\n"
        f"Message: '{msg}'\nCategory:"
    )
    intent = response.content.strip().lower()
    # Normalize
    for valid in ["billing", "shipping", "product", "account"]:
        if valid in intent:
            return {**state, "intent": valid}
    return {**state, "intent": "escalate"}

def handle_billing(state: SupportState) -> SupportState:
    """Handle billing-related queries."""
    # In production: call billing tools, check payment status
    context = "Billing context: Full refund within 7 days. EMI available above β‚Ή3,000."
    return {**state, "context": context}

def handle_shipping(state: SupportState) -> SupportState:
    """Handle shipping-related queries."""
    context = "Shipping context: Standard 5-7 days. Express 1-2 days. Delay >2 days = β‚Ή200 credit."
    return {**state, "context": context}

def handle_product(state: SupportState) -> SupportState:
    """Handle product-related queries."""
    context = "Product context: 1-year warranty on electronics. Damaged = full refund + β‚Ή500 credit."
    return {**state, "context": context}

def handle_escalation(state: SupportState) -> SupportState:
    """Escalate to human agent."""
    return {**state, "context": "ESCALATED: Ticket created for human review.",
            "final_response": "I understand this needs special attention. "
                             "I've escalated your case to a specialist who will "
                             "respond within 2 hours. Ticket ID: ESC-2026-0428."}

def generate_response(state: SupportState) -> SupportState:
    """Generate the customer-facing response."""
    if state.get("final_response"):
        return state  # Already set by escalation
    
    response = llm.invoke(
        f"You are a ShopEasy support agent. Generate a helpful reply.\n\n"
        f"Customer: {state['customer_message']}\n"
        f"Category: {state['intent']}\n"
        f"Policy Info: {state['context']}\n\n"
        f"Reply (empathetic, specific, under 100 words):"
    )
    return {**state, "final_response": response.content}

# --- Router Function ---
def route_by_intent(state: SupportState) -> Literal["billing", "shipping", "product", "escalate"]:
    """Route to the appropriate handler based on intent."""
    intent = state.get("intent", "escalate")
    if intent in ("billing", "shipping", "product"):
        return intent
    return "escalate"

# --- Build Graph ---
workflow = StateGraph(SupportState)

# Add nodes
workflow.add_node("classify", classify_intent)
workflow.add_node("billing", handle_billing)
workflow.add_node("shipping", handle_shipping)
workflow.add_node("product", handle_product)
workflow.add_node("escalate", handle_escalation)
workflow.add_node("respond", generate_response)

# Add edges
workflow.add_edge(START, "classify")
workflow.add_conditional_edges("classify", route_by_intent, {
    "billing": "billing",
    "shipping": "shipping",
    "product": "product",
    "escalate": "escalate",
})
workflow.add_edge("billing", "respond")
workflow.add_edge("shipping", "respond")
workflow.add_edge("product", "respond")
workflow.add_edge("escalate", "respond")
workflow.add_edge("respond", END)

# Compile
app = workflow.compile()

# --- Run ---
result = app.invoke({
    "customer_message": "My laptop screen is cracked after just 2 weeks!",
    "intent": "", "context": "", "sentiment": "",
    "draft_response": "", "final_response": ""
})
print(f"Intent: {result['intent']}")
print(f"Response: {result['final_response']}")

πŸ”€ Human-in-the-Loop

For sensitive actions (refunds > β‚Ή5,000, account deletion), pause the graph and wait for human approval.

Python β€” Interrupt for Human Approval
from langgraph.checkpoint.memory import MemorySaver

# Add a checkpoint saver
checkpointer = MemorySaver()

# Compile with interrupt_before
app = workflow.compile(
    checkpointer=checkpointer,
    interrupt_before=["escalate"]  # Pause before escalation
)

# Run β€” will pause at "escalate" node
config = {"configurable": {"thread_id": "ticket-001"}}
result = app.invoke({
    "customer_message": "I want to sue your company!",
    "intent": "", "context": "", "sentiment": "",
    "draft_response": "", "final_response": ""
}, config)

# At this point, graph is PAUSED
# Human reviews and approves...
print("⏸️ Paused for human review")

# Resume after approval
result = app.invoke(None, config)  # Continue from checkpoint
print(f"Response: {result['final_response']}")
🎧 Use Case Connection

Customer Support β€” When to Use LangGraph

ScenarioWhy LangGraph?
Different handlers per intentConditional routing β€” billing team vs shipping team
Refund > β‚Ή5,000Human-in-the-loop β€” manager approval required
Complex multi-issue ticketParallel nodes β€” handle billing + shipping simultaneously
Quality assurancePost-processing node β€” check response before sending
Retry on failureCheckpointing β€” save state and retry from last good point

βœ… Key Takeaways

  • LangGraph models agent workflows as directed graphs (nodes + edges + state)
  • Conditional edges enable routing based on classification or business rules
  • Human-in-the-loop with interrupt_before/after for sensitive actions
  • Checkpointing enables save/resume and retry
  • Use LangGraph when AgentExecutor is too simple for your workflow

πŸ”¨ Hands-On Tasks

  1. Build the support workflow graph with 4 intent handlers
  2. Test with 5 different messages and verify correct routing
  3. Add a human-in-the-loop interrupt for high-value refunds
  4. Add a "quality_check" node that rejects responses containing forbidden words
DAY 15
Multi-Agent Systems β€” CrewAI & Collaboration
⏱️ 45 min πŸ’» Build + Theory 🎯 Agents working together

🎯 Learning Objectives

πŸ€” Why Multiple Agents?

A single agent trying to do everything is like one employee handling triage, investigation, response drafting, AND quality review. Specialization beats generalization for complex workflows.

❌ Single Agent (Does Everything)

  • One massive system prompt β€” confusing
  • Too many tools β€” LLM picks wrong one
  • No separation of concerns
  • Hard to debug β€” "why did it do that?"
  • Can't specialize per task

βœ… Multi-Agent (Specialized Roles)

  • Each agent has focused prompt
  • Each agent has only its own tools
  • Clear responsibility per agent
  • Easy to trace which agent failed
  • Different LLMs per agent if needed

πŸ‘₯ CrewAI β€” Core Concepts

πŸ€–
Agent
A specialized role
with tools, goal, backstory
πŸ“‹
Task
A specific job
assigned to an agent
πŸ‘₯
Crew
A team of agents
working together
πŸ”„
Process
How the crew operates
(sequential/hierarchical)

πŸ—οΈ Customer Support Crew

3-Agent Support Crew
🏷️ Triage Agent
Classifies intent
Extracts entities
Assesses urgency
β†’
πŸ” Research Agent
Looks up orders
Searches knowledge base
Gathers all facts
β†’
πŸ’¬ Response Agent
Drafts reply
Applies guardrails
Ensures quality
Python β€” CrewAI Support Crew
from crewai import Agent, Task, Crew, Process
from crewai_tools import tool
from langchain_ollama import ChatOllama
import json

llm = ChatOllama(model="qwen2.5:3b", temperature=0.2)

# --- Tools ---
@tool("Order Lookup")
def lookup_order(order_id: str) -> str:
    """Look up order details by order ID."""
    orders = {
        "7845": {"order_id": "7845", "status": "shipped", "eta": "2026-04-30",
                 "items": ["MacBook Air M3"], "total": 89999},
        "7846": {"order_id": "7846", "status": "delivered",
                 "items": ["iPhone 16"], "total": 79999},
    }
    return json.dumps(orders.get(order_id.strip(), {"error": "Order not found"}))

@tool("Knowledge Base Search")
def search_kb(query: str) -> str:
    """Search ShopEasy knowledge base for policies and FAQs."""
    policies = {
        "refund": "Full refund within 7 days. After 7 days: store credit. Electronics: 15 days.",
        "shipping": "Standard 5-7 days. Express 1-2 days. Delay >2 days = β‚Ή200 credit.",
        "damaged": "Damaged: full refund + β‚Ή500 credit. Photo required.",
        "warranty": "Electronics: 1-year warranty. Extended warranty β‚Ή999/year."
    }
    results = [v for k, v in policies.items() if any(w in query.lower() for w in k.split())]
    return "\n".join(results) if results else "No relevant policy found."

# --- Agent 1: Triage ---
triage_agent = Agent(
    role="Customer Triage Specialist",
    goal="Quickly classify customer issues, extract key entities, and assess urgency",
    backstory="""You are the first line of support at ShopEasy. You read customer 
    messages and immediately identify: the intent (billing/shipping/product/account), 
    any order IDs or product names, the customer's sentiment, and urgency level.""",
    llm=llm,
    verbose=True
)

# --- Agent 2: Research ---
research_agent = Agent(
    role="Support Research Analyst",
    goal="Gather all relevant information needed to resolve the customer's issue",
    backstory="""You are a thorough researcher. Given a triaged ticket, you look up 
    order details, search the knowledge base for relevant policies, and compile 
    all facts needed for a resolution.""",
    tools=[lookup_order, search_kb],
    llm=llm,
    verbose=True
)

# --- Agent 3: Response ---
response_agent = Agent(
    role="Customer Response Writer",
    goal="Write empathetic, accurate, professional responses that resolve customer issues",
    backstory="""You are an expert at writing customer-facing messages. You take 
    research findings and craft the perfect response β€” empathetic, specific (with 
    order numbers and amounts), and actionable. You never make up information.""",
    llm=llm,
    verbose=True
)

# --- Tasks ---
def create_support_tasks(customer_message: str):
    triage_task = Task(
        description=f"""Analyze this customer message:
        "{customer_message}"
        
        Provide: intent, entities (order IDs, products), sentiment, urgency (low/med/high/critical)""",
        expected_output="JSON with intent, entities, sentiment, urgency",
        agent=triage_agent
    )

    research_task = Task(
        description="""Based on the triage analysis, gather all information needed:
        - Look up any order IDs mentioned
        - Search knowledge base for relevant policies
        - Compile complete context for response""",
        expected_output="Complete research findings with order details and applicable policies",
        agent=research_agent,
        context=[triage_task]  # Depends on triage output
    )

    response_task = Task(
        description="""Write the customer response using the research findings.
        Rules: empathetic, specific (include order numbers/amounts), under 100 words,
        actionable next steps. Never fabricate information.""",
        expected_output="A professional customer-facing response",
        agent=response_agent,
        context=[triage_task, research_task]  # Has all context
    )
    
    return [triage_task, research_task, response_task]

# --- Create Crew ---
tasks = create_support_tasks("I ordered a MacBook (order #7845) and it still hasn't arrived. Very frustrated!")

crew = Crew(
    agents=[triage_agent, research_agent, response_agent],
    tasks=tasks,
    process=Process.sequential,  # One after another
    verbose=True
)

# --- Run ---
result = crew.kickoff()
print(f"\n{'='*60}")
print(f"Final Response:\n{result}")

πŸ”„ Orchestration Patterns

PatternHow It WorksBest For
SequentialAgent A β†’ Agent B β†’ Agent C (pipeline)Triage β†’ Research β†’ Respond
HierarchicalManager agent delegates to specialistsManager routes to billing/shipping/product team
ParallelMultiple agents work simultaneouslySearch KB + lookup order at the same time
ConsensusMultiple agents vote on the best answerQA: 3 agents draft response, pick the best
Python β€” Hierarchical Process
# Manager agent decides which specialist to use
crew = Crew(
    agents=[triage_agent, research_agent, response_agent],
    tasks=tasks,
    process=Process.hierarchical,  # Manager delegates
    manager_llm=ChatOllama(model="qwen2.5:3b"),
    verbose=True
)

🏒 Scaling: Department-Based Multi-Crew

Enterprise Support β€” Multi-Crew Architecture
πŸ“¨ Incoming Ticket
↓
🏷️ Triage Crew (classify + route)
↓ route by department
πŸ’° Billing Crew
Payment Agent
Refund Agent
Invoice Agent
πŸ“¦ Shipping Crew
Tracking Agent
Delivery Agent
Return Agent
πŸ”§ Product Crew
Warranty Agent
Tech Support Agent
Replacement Agent
↓ all converge
βœ… QA Crew β€” Review response before sending
🎧 Use Case Connection

Customer Support β€” When to Go Multi-Agent

Single Agent βœ…Multi-Agent βœ…
Simple order status checkComplex complaint (multiple issues)
FAQ / policy questionsTickets needing research + response + QA
Low-volume supportHigh-volume: route to specialized teams
Prototype / MVPProduction system with quality gates

βœ… Key Takeaways

  • Multi-agent = specialization β€” each agent has a focused role and tools
  • CrewAI makes it easy: define Agents β†’ Tasks β†’ Crew β†’ Process
  • Sequential process: pipeline (A β†’ B β†’ C). Hierarchical: manager delegates
  • Task context chains let later agents use earlier agents' output
  • Scale by creating department-based crews for enterprise support

πŸ”¨ Hands-On Tasks

  1. Build the 3-agent support crew (Triage β†’ Research β†’ Response)
  2. Test with 5 different customer scenarios and trace each agent's work
  3. Switch from sequential to hierarchical process β€” observe the difference
  4. Add a 4th "QA Agent" that reviews the response before it's sent
WEEKEND
Weekend: RAG Support Agent v2
⏱️ 3-4 hours πŸ’» Full Project 🎯 Production-ready prototype

🎯 Assignment: Build Customer Support Agent v2

Upgrade your Week 2 agent with RAG, LangGraph workflows, and a Streamlit UI.

Architecture

Agent v2 β€” Target Architecture
πŸ–₯️ Streamlit UI β€” Chat interface with conversation history
↓
πŸ”€ LangGraph Workflow β€” classify β†’ route β†’ handle β†’ respond
↓
πŸ” RAG
ChromaDB
Policy search
πŸ”§ Tools
Order lookup
Email send
πŸ’Ύ Memory
Window + Vector
Conversation
↓
🧠 Qwen 2.5:3B via Ollama β€” Local, free, private

Requirements

#FeatureDetails
1RAG Knowledge BaseIngest policies.txt + faqs.txt into ChromaDB. At least 20 chunks.
2LangGraph Workflowclassify β†’ route (billing/shipping/product/escalate) β†’ handle β†’ respond
3Tools (min 4)search_kb, lookup_order, get_refund_policy, escalate_to_human
4MemoryConversationBufferWindowMemory (k=5) for multi-turn
5Streamlit UIChat interface with message history display
6Verbose LoggingPrint agent reasoning to terminal for debugging

Test Scenarios

Required test cases
# 1. Policy Query (RAG)
"What's your return policy for electronics?"
β†’ Should search KB and cite: "15-day return window for electronics"

# 2. Order + Policy Combo
"My order #7845 arrived damaged. What can I do?"
β†’ lookup_order + search_kb("damaged") β†’ Offer refund + β‚Ή500 credit

# 3. Multi-Turn Memory
Turn 1: "I'm Rajesh, order #7845"
Turn 2: "Is it eligible for a refund?"
Turn 3: "Ok, process it"
β†’ Agent remembers Rajesh and order across turns

# 4. Correct Routing
"I was charged twice" β†’ billing handler
"Package not delivered" β†’ shipping handler
"Phone screen cracked" β†’ product handler

# 5. Escalation
"I want to speak to a manager"
β†’ Should escalate, not try to handle

Bonus Challenges

  1. Add a multi-agent crew (Triage β†’ Research β†’ Response) as an alternative to single agent
  2. Add Streamlit sidebar showing: detected intent, sentiment, tools called
  3. Implement basic analytics: count of tickets by category, avg response time
  4. Load real FAQ data from a CSV/JSON file instead of hardcoded
πŸ†
Success criteria: All 5 test scenarios pass. RAG returns accurate policy info. LangGraph routes correctly by intent. Memory persists across turns. Streamlit UI shows clean chat history.
βœ… Week 3 Complete β€” You Can Build Real Agents!

Final week: Week 4 β€” Integration & Production β†’ Semantic Kernel (.NET), Angular + .NET + Agent, Testing, Deployment