Skip to content

Migration Guides

Side-by-side comparisons with LangChain, CrewAI, AutoGen, and LlamaIndex. Every example shows the other framework's way and the selectools equivalent.


Coming from LangChain / LangGraph


Tool Calling

LangChain:

from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

@tool
def search(query: str) -> str:
    """Search the web."""
    return f"Results for: {query}"

llm = ChatOpenAI(model="gpt-4o")
llm_with_tools = llm.bind_tools([search])
result = llm_with_tools.invoke("Search for Python tutorials")

selectools:

from selectools import Agent, AgentConfig, OpenAIProvider, tool

@tool(description="Search the web")
def search(query: str) -> str:
    return f"Results for: {query}"

agent = Agent(tools=[search], provider=OpenAIProvider())
result = agent.run("Search for Python tutorials")
print(result.content)      # The answer
print(result.reasoning)    # Why it chose that tool
print(result.trace)        # Full execution timeline

What's different: selectools gives you result.reasoning and result.trace for free. No LangSmith needed.


Multi-Agent Graph

LangGraph:

from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict

class State(TypedDict):
    text: str

def planner(state): return {"text": "planned"}
def writer(state): return {"text": "written"}
def reviewer(state): return {"text": "reviewed"}

g = StateGraph(State)
g.add_node("planner", planner)
g.add_node("writer", writer)
g.add_node("reviewer", reviewer)
g.add_edge(START, "planner")
g.add_edge("planner", "writer")
g.add_edge("writer", "reviewer")
g.add_edge("reviewer", END)
app = g.compile()
result = app.invoke({"text": "prompt"})

selectools:

from selectools import AgentGraph

result = AgentGraph.chain(planner, writer, reviewer).run("prompt")

What's different: No StateGraph, no TypedDict, no compile(). Plain Python.


Conditional Routing

LangGraph:

def should_continue(state):
    if state["needs_review"]:
        return "reviewer"
    return END

g.add_conditional_edges("writer", should_continue, {
    "reviewer": "reviewer",
    END: END,
})

selectools:

graph.add_conditional_edge(
    "writer",
    lambda state: "reviewer" if state.data.get("needs_review") else AgentGraph.END,
)

What's different: No path_map required. The function returns a node name directly.


Human-in-the-Loop

LangGraph:

# Node restarts from the top on resume — guard expensive work manually
def review_node(state):
    if "analysis" not in state:
        state["analysis"] = expensive_llm_call(state["draft"])  # runs TWICE without guard
    return Command(goto="human_input")

selectools:

# Generator pauses at yield, resumes at exact yield point
async def review_node(state):
    analysis = await expensive_llm_call(state.data["draft"])  # runs ONCE
    decision = yield InterruptRequest(prompt="Approve?", payload=analysis)
    state.data["approved"] = decision == "yes"

What's different: No manual if key not in state guards. The generator preserves local variables across pause/resume.


Streaming

LangChain (LCEL):

chain = prompt | llm | parser
async for chunk in chain.astream({"topic": "AI"}):
    print(chunk, end="")

selectools:

async for item in agent.astream("Tell me about AI"):
    if isinstance(item, str):
        print(item, end="")  # Text chunk
    elif isinstance(item, AgentResult):
        print(f"\nDone: {item.iterations} iterations")

What's different: astream() yields both text chunks AND tool calls natively. No separate streaming modes.


Composable Pipelines

LangChain (LCEL):

from langchain_core.runnables import RunnablePassthrough, RunnableParallel

chain = (
    RunnableParallel(context=retriever, question=RunnablePassthrough())
    | prompt
    | llm
    | parser
)

selectools:

from selectools import step, parallel, branch

@step
def summarize(text: str) -> str:
    return agent.run(f"Summarize: {text}").content

@step
def translate(text: str) -> str:
    return agent.run(f"Translate: {text}").content

pipeline = summarize | translate
result = pipeline.run("Long article...")

What's different: Steps are plain functions. No Runnable base class, no RunnablePassthrough. When it breaks, you get a Python traceback.


Evaluation

LangChain: Requires LangSmith (paid SaaS).

selectools:

from selectools.evals import EvalSuite, TestCase

suite = EvalSuite(agent=agent, cases=[
    TestCase(input="Cancel account", expect_tool="cancel_sub"),
    TestCase(input="Balance?", expect_contains="balance"),
])
report = suite.run()
report.to_html("report.html")

What's different: 50 evaluators built into the library. No paid service, no separate install.


Deployment

LangChain: pip install langserve + FastAPI boilerplate + add_routes().

selectools:

selectools serve agent.yaml

That's it. HTTP API + SSE streaming + playground UI. Or in Python:

from selectools.serve import create_app
app = create_app(agent, playground=True)
app.serve(port=8000)

Cost Tracking

LangChain: Manual. Use callbacks or LangSmith.

selectools:

result = agent.run("Search and summarize")
print(f"Cost: ${result.usage.total_cost_usd:.4f}")
print(f"Tokens: {result.usage.total_tokens}")

Automatic per-call cost tracking across 152 models with built-in pricing data.


What LangChain Does Better (honest)

  • Ecosystem size — hundreds of integrations, community answers everywhere
  • LangSmith — if you want hosted tracing/evals, it's polished
  • Maturity — battle-tested at thousands of companies
  • LangGraph Platform — managed deployment with cron, webhooks, SSO

If you need a managed platform or 50+ integrations today, LangChain is the safer bet. If you want a library that stays out of your way and includes everything in one package, give selectools a try.

---

Coming from CrewAI

CrewAI uses role-based agents with a Crew coordinator. Selectools uses graph-based orchestration where any agent can route to any other.


Agent Definition

CrewAI:

from crewai import Agent, Task, Crew

researcher = Agent(
    role="Researcher",
    goal="Find accurate information",
    backstory="You are an expert researcher...",
    llm="gpt-4o",
)
writer = Agent(
    role="Writer",
    goal="Write clear content",
    backstory="You are a skilled writer...",
    llm="gpt-4o",
)

task1 = Task(description="Research AI trends", agent=researcher)
task2 = Task(description="Write a report", agent=writer)

crew = Crew(agents=[researcher, writer], tasks=[task1, task2])
result = crew.kickoff()

selectools:

from selectools import Agent, AgentConfig, AgentGraph
from selectools.providers import OpenAIProvider

provider = OpenAIProvider()
researcher = Agent(
    tools=[search],
    provider=provider,
    config=AgentConfig(model="gpt-4o", system_prompt="You are an expert researcher."),
)
writer = Agent(
    tools=[],
    provider=provider,
    config=AgentConfig(model="gpt-4o", system_prompt="You are a skilled writer."),
)

graph = AgentGraph()
graph.add_node("researcher", researcher)
graph.add_node("writer", writer)
graph.add_edge("START", "researcher")
graph.add_edge("researcher", "writer")
graph.add_edge("writer", "END")
result = graph.run("Research AI trends and write a report")

What's different: No role/goal/backstory boilerplate. System prompts are plain strings. Graphs give you conditional routing, parallel execution, and HITL that CrewAI's sequential task model doesn't support.


What CrewAI Does Better (honest)

  • Simpler mental model for sequential task chains (no graph concepts)
  • Role-based prompting is automatic (role/goal/backstory templating)
  • Enterprise plan includes hosted orchestration

---

Coming from AutoGen

AutoGen uses conversational agents that chat with each other. Selectools uses directed graphs with explicit routing.


Multi-Agent Chat

AutoGen:

from autogen import AssistantAgent, UserProxyAgent

assistant = AssistantAgent("assistant", llm_config={"model": "gpt-4o"})
user_proxy = UserProxyAgent("user", code_execution_config={"work_dir": "coding"})

user_proxy.initiate_chat(assistant, message="Write a Python script")

selectools:

from selectools import Agent, AgentConfig
from selectools.providers import OpenAIProvider

agent = Agent(
    tools=[],
    provider=OpenAIProvider(),
    config=AgentConfig(model="gpt-4o"),
)
result = agent.run("Write a Python script")
print(result.content)

What's different: selectools doesn't use agent-to-agent chat. Instead, you compose agents into graphs where data flows through explicit edges. This is more predictable than open-ended conversations between agents.


Group Chat (AutoGen) vs AgentGraph (selectools)

AutoGen:

from autogen import GroupChat, GroupChatManager

group = GroupChat(agents=[agent1, agent2, agent3], messages=[], max_round=10)
manager = GroupChatManager(groupchat=group, llm_config=config)
user_proxy.initiate_chat(manager, message="Solve this problem")

selectools:

from selectools.orchestration import SupervisorAgent

supervisor = SupervisorAgent(
    agents={"researcher": agent1, "writer": agent2, "reviewer": agent3},
    strategy="dynamic",  # LLM picks the best agent each step
    provider=provider,
)
result = supervisor.run("Solve this problem")

What's different: SupervisorAgent gives you 4 coordination strategies (plan_and_execute, round_robin, dynamic, magentic) instead of AutoGen's single group chat model. The LLM router in dynamic mode is similar to AutoGen's speaker selection but with explicit control.


What AutoGen Does Better (honest)

  • Code execution is built in (Docker sandboxes)
  • Agent-to-agent conversation is natural for brainstorming/debate scenarios
  • Microsoft ecosystem integration

---

Coming from LlamaIndex

LlamaIndex focuses on data indexing and retrieval. Selectools has a built-in RAG pipeline but also covers agent orchestration, evals, and deployment.


RAG Pipeline

LlamaIndex:

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader

documents = SimpleDirectoryReader("data").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
response = query_engine.query("What is the refund policy?")

selectools:

from selectools.rag import DocumentLoader, TextSplitter, InMemoryVectorStore
from selectools.embeddings import OpenAIEmbeddings

docs = DocumentLoader.from_directory("data")
chunks = TextSplitter(chunk_size=500).split_documents(docs)
store = InMemoryVectorStore(embeddings=OpenAIEmbeddings())
store.add_documents(chunks)

results = store.search("What is the refund policy?", top_k=5)

What's different: selectools exposes every step (chunking, embedding, retrieval, reranking) as a composable piece. You can swap BM25 for vector search, add a reranker, or use hybrid search with RRF fusion. LlamaIndex's VectorStoreIndex hides these choices.


LlamaIndex: Requires BM25Retriever + QueryFusionRetriever with manual setup.

selectools:

from selectools.rag import HybridSearcher, BM25Index

searcher = HybridSearcher(
    vector_store=store,
    bm25_index=BM25Index(chunks),
    alpha=0.5,  # balance between BM25 and vector
)
results = searcher.search("refund policy", top_k=10)

What's different: Hybrid search is a first-class feature, not an afterthought. Built-in RRF fusion and cross-encoder reranking.


What LlamaIndex Does Better (honest)

  • Data connectors for 100+ sources (Notion, Google Drive, Slack, databases)
  • Advanced indexing (tree, keyword, knowledge graph indexes)
  • Mature RAG ecosystem with years of optimization
  • LlamaParse for complex document parsing (tables, PDFs)

If your primary need is sophisticated document retrieval with many data sources, LlamaIndex is purpose-built for that. If you need agents + RAG + evals + deployment in one package, selectools combines all of these.