# Install required packages
# !pip install langchain langchain-openai langgraph chromadb

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_functions_agent
from langchain.tools import Tool
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.memory import ConversationBufferMemory

load_dotenv()
print("βœ… Setup complete")

Part 1: Framework Overview (2026 Landscape)ΒΆ

The agent ecosystem now spans fully managed cloud APIs, open-source SDKs/frameworks, and interop protocols that let agents share tools and talk to each other.

Fully Managed Agent APIsΒΆ

These platforms host and run agents for you β€” bring your prompt and tools, they handle infra, scaling, and monitoring.

Service

Vendor

Model Lock-in

Key Differentiator

Anthropic Managed Agents

Anthropic

Claude only

Versioned agent configs, hosted infra, built-in tool suite

LangGraph Cloud

LangChain

Model-agnostic

Hosted deployment of LangGraph agents, persistence, streaming

AWS Bedrock Agents

AWS

Multi-model (Bedrock)

Native AWS integrations (S3, Lambda, Knowledge Bases)

Azure AI Agent Service

Microsoft (Foundry)

Multi-model (Azure)

Tool connectors (Azure Search, Code Interpreter, Bing), enterprise RBAC

Vertex AI Agent Builder

Google

Gemini-native

GCP-native, integrated with Vertex AI Search and Conversation

Salesforce Agentforce

Salesforce

Multi-model

Enterprise agents embedded in CRM workflows

Open-Source SDKs & FrameworksΒΆ

Framework

Best For

Learning Curve

Key Differentiator

LangChain

General-purpose agents, quick prototypes

Easy

Largest ecosystem of tools and integrations

LangGraph

Complex workflows, cyclic graphs

Medium-Hard

Graph-based orchestration, most mature for stateful agents

OpenAI Agents SDK

Lightweight multi-agent handoffs

Easy

Minimal abstractions, built-in tracing, provider-agnostic via LiteLLM

Google ADK

GCP-native agents, multi-language

Medium

4 language SDKs (Python, Java, Go, Node), A2A protocol

CrewAI

Multi-agent teams, role-based agents

Easy

Easiest onboarding, role-based collaboration model

AutoGen

Multi-agent conversation/debate

Medium

Microsoft-backed, multi-agent conversation and debate patterns

Semantic Kernel

Enterprise .NET + Python agents

Medium

Deep Azure integration, .NET first-class support, plugin system

SmolAgents

Minimal code-first agents

Easy

Hugging Face, <100 lines to a working agent, code-generation agents

Llama-stack

Meta/Llama ecosystem

Medium

Safety built-in, Llama-native tooling, on-device support

Haystack

RAG-heavy agent pipelines

Medium

deepset, pipeline-based, strong retrieval integration

Bee Agent Framework

Production enterprise agents

Medium

IBM-backed, observability and compliance focus

Custom

Full control, specific requirements

Hard

No abstractions, maximum flexibility

Interop Protocols & StandardsΒΆ

Protocol

Purpose

Status (2026)

MCP (Model Context Protocol)

Standardized tool/resource connectivity for agents

Broadly adopted β€” Anthropic, OpenAI, Google, Microsoft all support it

A2A (Agent-to-Agent, Google)

Agent interoperability β€” agents discovering and delegating to other agents

Early adoption β€” Google ADK native, growing cross-vendor support

See Notebook 06 for deep coverage of MCP.

Visual / No-Code Agent BuildersΒΆ

For teams that want to build agent workflows without writing Python, several platforms provide drag-and-drop graph editors:

Platform

Type

Key Feature

Best For

Langflow

Open-source

Visual LangChain/LangGraph builder, runs locally or on Datastax cloud

Prototyping LangChain flows, export to Python

Flowise

Open-source

Low-code LLM app builder, Node.js, Docker-ready

Quick chatbot/agent demos, internal tools

Dify

Open-source

Full RAG + agent platform with visual workflow editor

End-to-end LLM apps, teams with mixed skill levels

n8n AI

Open-source + cloud

Workflow automation with AI agent nodes

Connecting agents to 400+ SaaS integrations

When to choose no-code: Rapid prototyping, non-developer stakeholders defining workflows, or simple agent use cases that don’t require custom Python logic.

When to Use EachΒΆ

LangChain:

  • βœ… Quick prototypes

  • βœ… Standard agent patterns

  • βœ… Rich ecosystem of tools

  • ❌ Limited control over agent loop

LangGraph:

  • βœ… Complex state machines

  • βœ… Cyclic workflows

  • βœ… Human-in-the-loop

  • ❌ Steepest learning curve

OpenAI Agents SDK:

  • βœ… Lightweight, minimal boilerplate

  • βœ… Multi-agent handoffs built-in

  • βœ… Provider-agnostic (LiteLLM)

  • ❌ Less mature ecosystem than LangChain

Google ADK:

  • βœ… Multi-language (Python, Java, Go, Node)

  • βœ… Native A2A protocol support

  • βœ… GCP integration

  • ❌ Smaller community than LangChain/OpenAI

CrewAI:

  • βœ… Role-based collaboration

  • βœ… Easiest onboarding

  • ❌ Less flexible for non-team patterns

Semantic Kernel:

  • βœ… .NET first-class support

  • βœ… Deep Azure/Microsoft 365 integration

  • ❌ Smaller Python community

Custom Implementation:

  • βœ… Full control

  • βœ… Optimized for specific use case

  • ❌ More development time

Managed vs. Self-Hosted: Key QuestionsΒΆ

  1. Cost vs. control β€” Are you willing to pay for fully managed agent infra (Anthropic, Azure, AWS), or do you prefer owning the stack?

  2. Lock-in β€” Anthropic Managed Agents ties you to Claude. Azure ties you to Azure. Does model/cloud flexibility matter?

  3. Multi-agent in prod β€” Running multi-agent setups? LangGraph and AutoGen handle this best. CrewAI is easiest to start.

  4. Interop β€” Does your architecture need MCP (tool sharing) or A2A (agent delegation)? These are becoming table stakes for multi-vendor setups.

Part 2: LangChain AgentsΒΆ

Building Your First LangChain AgentΒΆ

LangChain abstracts the agent loop into two core components: an Agent (the LLM reasoning engine that decides which tool to call) and an AgentExecutor (the runtime that manages the observe-think-act cycle, handles tool dispatch, and enforces iteration limits). The create_openai_functions_agent constructor wires the LLM to OpenAI’s native function-calling API, so the model returns structured JSON tool invocations rather than free-text that must be parsed with brittle regex.

How LangChain Agents WorkΒΆ

The executor runs a loop: (1) pass the conversation plus a scratchpad of prior tool calls/results to the LLM, (2) if the LLM returns a function call, execute the matching Tool and append the result to the scratchpad, (3) repeat until the LLM returns a plain text answer or max_iterations is reached. This is functionally equivalent to the ReAct pattern from Notebook 03, but the framework handles prompt formatting, output parsing, and error recovery. The Tool wrapper maps a Python callable to a name and natural-language description that the LLM uses to decide when invocation is appropriate, making tool registration a one-liner rather than a manual JSON schema.

# Initialize LLM
llm = ChatOpenAI(model="gpt-4", temperature=0)

# Define tools
def get_word_length(word: str) -> int:
    """Returns the length of a word."""
    return len(word)

def multiply_numbers(a: float, b: float) -> float:
    """Multiply two numbers together."""
    return a * b

# Create LangChain tools
tools = [
    Tool(
        name="get_word_length",
        func=get_word_length,
        description="Get the length of any word. Input should be a single word."
    ),
    Tool(
        name="multiply",
        func=lambda x: multiply_numbers(*map(float, x.split(','))),
        description="Multiply two numbers. Input should be two numbers separated by comma, e.g., '5,3'"
    )
]

print(f"βœ… Created {len(tools)} tools")
# Create agent prompt
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant with access to tools."),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad")
])

# Create agent
agent = create_openai_functions_agent(llm, tools, prompt)

# Create agent executor
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    max_iterations=5
)

print("βœ… LangChain agent ready")
# Test the agent
response = agent_executor.invoke({
    "input": "What is the length of the word 'LangChain' multiplied by 3?"
})

print(f"\nπŸ€– Agent Response: {response['output']}")

Real-World Example: Research AgentΒΆ

A research agent demonstrates how multiple tools compose to answer questions that no single tool could handle alone. The agent below has access to Wikipedia for factual retrieval, a calculator for arithmetic, and a date tool for temporal context. When the user asks a compound question like β€œHow many years ago was Python created?”, the agent autonomously chains tool calls: first querying Wikipedia for the creation date, then calling the calculator to subtract from today’s date.

Why This Pattern MattersΒΆ

Production LLM applications rarely need just one capability. By registering heterogeneous tools with clear, descriptive docstrings, you let the model’s function-calling mechanism serve as an implicit router that selects the right tool based on semantic understanding of the query. The quality of tool descriptions directly affects routing accuracy – vague descriptions cause the agent to pick the wrong tool or hallucinate answers instead of calling any tool at all. Each Tool object’s description field is effectively part of the prompt, so treat it with the same care you would give a system message.

import wikipedia
from datetime import datetime

# Wikipedia search tool
def search_wikipedia(query: str) -> str:
    """Search Wikipedia and return a summary."""
    try:
        return wikipedia.summary(query, sentences=3)
    except:
        return f"Could not find information about '{query}'"

# Current date tool
def get_current_date() -> str:
    """Get the current date."""
    return datetime.now().strftime("%B %d, %Y")

# Calculator tool
def calculate(expression: str) -> str:
    """Evaluate a mathematical expression."""
    try:
        result = eval(expression)
        return str(result)
    except:
        return "Invalid expression"

# Create research tools
research_tools = [
    Tool(
        name="wikipedia",
        func=search_wikipedia,
        description="Search Wikipedia for information. Input should be a search query."
    ),
    Tool(
        name="current_date",
        func=get_current_date,
        description="Get today's date. No input required."
    ),
    Tool(
        name="calculator",
        func=calculate,
        description="Calculate mathematical expressions. Input should be a valid math expression."
    )
]

# Create research agent
research_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a research assistant. Answer questions using available tools."),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad")
])

research_agent = create_openai_functions_agent(llm, research_tools, research_prompt)
research_executor = AgentExecutor(
    agent=research_agent,
    tools=research_tools,
    verbose=True
)

print("βœ… Research agent ready")
# Test research agent
result = research_executor.invoke({
    "input": "Who invented Python programming language and when?"
})

print(f"\nπŸ“š Research Result:\n{result['output']}")

Part 3: LangGraph WorkflowsΒΆ

From Linear Chains to Stateful GraphsΒΆ

LangGraph extends LangChain by modeling agent logic as a directed graph where nodes are processing steps and edges define transitions. Unlike a simple sequential chain, LangGraph supports cycles (an agent can loop back to re-plan after receiving new information), conditional branching (route to different nodes based on state), and human-in-the-loop checkpoints. The StateGraph class manages a typed state dictionary that flows through the graph, with each node reading and updating shared state via TypedDict annotations.

Why Graph-Based Orchestration MattersΒΆ

Many real-world agent tasks are not linear pipelines. A coding assistant might plan, write code, run tests, discover a bug, and loop back to rewrite – a cyclic workflow that cannot be expressed as a simple chain. LangGraph’s add_conditional_edges method lets you define routing functions that inspect the current state and choose the next node, enabling patterns like retry loops, parallel fan-out/fan-in, and early termination. The compile() step converts the graph definition into an executable Runnable that supports streaming, async execution, and state persistence for long-running workflows.

from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator

# Define state
class AgentState(TypedDict):
    messages: Annotated[list, operator.add]
    next_step: str
    final_answer: str

# Node functions
def planning_node(state: AgentState) -> AgentState:
    """Plan the approach"""
    print("🧠 Planning...")
    state["messages"].append("Created plan")
    state["next_step"] = "research"
    return state

def research_node(state: AgentState) -> AgentState:
    """Conduct research"""
    print("πŸ” Researching...")
    state["messages"].append("Gathered information")
    state["next_step"] = "synthesis"
    return state

def synthesis_node(state: AgentState) -> AgentState:
    """Synthesize findings"""
    print("✍️ Synthesizing...")
    state["final_answer"] = "Research complete with findings"
    state["next_step"] = "end"
    return state

# Build graph
workflow = StateGraph(AgentState)

# Add nodes
workflow.add_node("planning", planning_node)
workflow.add_node("research", research_node)
workflow.add_node("synthesis", synthesis_node)

# Add edges
workflow.set_entry_point("planning")
workflow.add_edge("planning", "research")
workflow.add_edge("research", "synthesis")
workflow.add_edge("synthesis", END)

# Compile
app = workflow.compile()

print("βœ… LangGraph workflow created")
# Run the workflow
initial_state = {
    "messages": ["Starting research task"],
    "next_step": "planning",
    "final_answer": ""
}

final_state = app.invoke(initial_state)

print("\nπŸ“Š Workflow Result:")
print(f"Messages: {final_state['messages']}")
print(f"Final Answer: {final_state['final_answer']}")

Part 4: Memory IntegrationΒΆ

Giving Agents Persistent ContextΒΆ

Without memory, every agent invocation is stateless – the LLM has no knowledge of prior turns. ConversationBufferMemory solves this by storing the full message history and injecting it into the prompt via the chat_history placeholder. This enables multi-turn interactions where the agent can reference earlier context (β€œWhat was my name?” or β€œUse the same format as before”).

Memory Strategies and Trade-offsΒΆ

Buffer memory is the simplest approach but grows linearly with conversation length, eventually exceeding the model’s context window. LangChain provides alternatives: ConversationSummaryMemory compresses older turns into a running summary (trading fidelity for token efficiency), ConversationBufferWindowMemory keeps only the last \(k\) turns, and VectorStoreMemory embeds messages for semantic retrieval of relevant history. Choosing the right memory strategy depends on your token budget, conversation length, and whether the agent needs exact recall or just topical awareness of past interactions.

from langchain.memory import ConversationBufferMemory

# Create memory
memory = ConversationBufferMemory(
    memory_key="chat_history",
    return_messages=True
)

# Create agent with memory
memory_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant. Remember previous conversation."),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad")
])

memory_agent = create_openai_functions_agent(llm, research_tools, memory_prompt)
memory_executor = AgentExecutor(
    agent=memory_agent,
    tools=research_tools,
    memory=memory,
    verbose=True
)

print("βœ… Agent with memory ready")
# Test memory
print("First message:")
response1 = memory_executor.invoke({"input": "My name is Alice"})
print(response1['output'])

print("\nSecond message (should remember name):")
response2 = memory_executor.invoke({"input": "What's my name?"})
print(response2['output'])

Part 5: Framework ComparisonΒΆ

LangChain vs Custom ImplementationΒΆ

The fundamental trade-off in agent frameworks is development speed versus control. LangChain lets you build a functional agent in 5-10 lines by composing pre-built abstractions (Tool, AgentExecutor, Memory), but those abstractions impose opinions about prompt formatting, error handling, and the agent loop that may not suit every use case. A custom implementation requires writing the full observe-think-act loop, tool dispatch, output parsing, and retry logic yourself – easily 50+ lines – but gives you complete visibility into every decision the agent makes.

When Custom WinsΒΆ

Custom implementations become worthwhile when you need fine-grained control over token budgets (e.g., dynamically pruning tool descriptions based on context), non-standard reasoning patterns (e.g., tree-of-thought with backtracking), or tight integration with proprietary infrastructure. In production systems where latency and cost matter, the overhead of framework abstractions – extra prompt tokens from verbose templates, unnecessary serialization steps – can add up. Profile both approaches on your actual workload before committing.

import time

# LangChain approach (5-10 lines)
def langchain_agent():
    tools = [Tool(name="calc", func=lambda x: eval(x), description="Calculator")]
    agent = create_openai_functions_agent(llm, tools, prompt)
    executor = AgentExecutor(agent=agent, tools=tools)
    return executor.invoke({"input": "What is 15 + 27?"})

# Custom approach (50+ lines)
def custom_agent():
    # Would need: prompt engineering, tool execution, loop control,
    # error handling, parsing, etc.
    pass

print("βœ… LangChain: Quick to build, less control")
print("βœ… Custom: More code, full control")

Decision Matrix (2026)ΒΆ

Criteria

LangChain

LangGraph

OpenAI Agents SDK

Google ADK

CrewAI

Semantic Kernel

Custom

Development Speed

⭐⭐⭐⭐⭐

⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐

⭐⭐⭐⭐⭐

⭐⭐⭐

⭐

Flexibility

⭐⭐⭐

⭐⭐⭐⭐⭐

⭐⭐⭐

⭐⭐⭐⭐

⭐⭐

⭐⭐⭐⭐

⭐⭐⭐⭐⭐

Multi-Agent

⭐⭐

⭐⭐⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐

⭐⭐⭐⭐⭐

MCP Support

⭐⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐⭐⭐

⭐⭐⭐⭐

⭐⭐

⭐⭐⭐⭐

⭐⭐

Model Agnostic

⭐⭐⭐⭐⭐

⭐⭐⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐⭐⭐

Documentation

⭐⭐⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐⭐

N/A

Community

⭐⭐⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐

⭐⭐⭐

⭐⭐⭐

⭐

Production Ready

⭐⭐⭐⭐

⭐⭐⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐

⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐⭐⭐

Part 6: Production PatternsΒΆ

Error Handling and RetriesΒΆ

Production agents face failures that never appear in tutorials: API rate limits, malformed tool outputs, infinite reasoning loops, and context window overflows. Wrapping the AgentExecutor in a custom subclass lets you intercept exceptions at the execution boundary, log diagnostic information, and return graceful fallback responses instead of crashing. The pattern below catches any exception during the agent loop and converts it into a structured error response that downstream code can handle.

Why Defensive Agent Design MattersΒΆ

An unhandled exception in an agent loop can leave the system in an inconsistent state – partial tool calls executed, memory corrupted, or user-facing errors exposed. Production agents should implement circuit breaker patterns (stop calling a failing tool after \(n\) consecutive errors), timeout guards (abort if the agent hasn’t converged within a time budget), and graceful degradation (fall back to a simpler model or direct response when tools are unavailable). The max_iterations parameter in AgentExecutor is your first line of defense against infinite loops, but application-level error handling provides the safety net.

from langchain.callbacks import StdOutCallbackHandler

# Add custom error handling
class SafeAgentExecutor(AgentExecutor):
    def _call(self, inputs, **kwargs):
        try:
            return super()._call(inputs, **kwargs)
        except Exception as e:
            return {
                "output": f"Error occurred: {str(e)}",
                "error": True
            }

print("βœ… Safe agent executor ready")

Monitoring and LoggingΒΆ

Structured logging transforms an opaque agent into an observable system. By subclassing AgentExecutor and logging inputs and outputs at the execution boundary, you create an audit trail that answers critical production questions: which tool was called, what arguments were passed, how long each step took, and whether the agent’s final answer addressed the user’s intent. LangChain’s callback system (StdOutCallbackHandler, LangSmithTracer) provides built-in hooks for tracing every intermediate step – tool invocations, LLM calls, token counts – without modifying agent code. In production, pipe these traces to an observability platform (Datadog, Weights & Biases, LangSmith) to monitor latency distributions, error rates, and cost per query across your agent fleet.

import logging

# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class LoggingAgentExecutor(AgentExecutor):
    def _call(self, inputs, **kwargs):
        logger.info(f"Agent input: {inputs}")
        result = super()._call(inputs, **kwargs)
        logger.info(f"Agent output: {result}")
        return result

print("βœ… Logging configured")

🎯 Knowledge Check¢

Q1: Name three fully managed agent API services and their vendor lock-in trade-offs.
Q2: When should you choose LangGraph over the OpenAI Agents SDK?
Q3: What are MCP and A2A, and why do they matter for multi-vendor agent architectures?
Q4: How does memory work in LangChain agents?

Click for answers

A1: Anthropic Managed Agents (Claude-only), AWS Bedrock Agents (multi-model on Bedrock), Azure AI Agent Service (multi-model on Azure). Lock-in increases with vendor-specific tool integrations.
A2: LangGraph when you need complex stateful workflows with cycles, conditional branching, human-in-the-loop checkpoints, or persistent state. OpenAI Agents SDK for lightweight multi-agent handoffs with minimal boilerplate.
A3: MCP (Model Context Protocol) standardizes how agents connect to tools/resources β€” broadly adopted across Anthropic, OpenAI, Google, Microsoft. A2A (Agent-to-Agent) enables agents to discover and delegate tasks to other agents β€” early but growing adoption. Both reduce vendor lock-in.
A4: Memory stores conversation history and passes it to the agent as context. Strategies include buffer (full history), summary (compressed), window (last k turns), and vector store (semantic retrieval).

πŸš€ Next StepsΒΆ

  1. Complete the Agent Framework Challenge

  2. Read Notebook 5: Multi-Agent Systems

  3. Read Notebook 6: MCP (Model Context Protocol)

  4. Read Notebook 7: OpenAI Agents SDK + LangGraph deep dive

  5. Build a production agent with your chosen framework

  6. Experiment with LangGraph for complex workflows

Great work! You now have a complete map of the 2026 agent framework landscape! πŸŽ‰