Coming from LangChain / LangGraph¶
Side-by-side migration guide. Every example shows the LangChain way and the selectools equivalent.
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: 39 evaluators built into the library. No paid service, no separate install.
Deployment¶
LangChain: pip install langserve + FastAPI boilerplate + add_routes().
selectools:
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.