Jangada AIJangada AI

Migrating from LangChain

This guide is for those who already have a LangChain project and want to move to jangada. It is not a "better/worse" comparison: the goal is to show, for each thing you did in LangChain, the equivalent pattern in jangada — which has its own design — and where the differentials are.

Philosophy. jangada is a thin layer over the official SDKs. Outside the adapters only two normalized types travel — Message and Completion. There is no Runnable/LCEL, no implicit graph: you call methods (complete, parse, stream) and compose with plain Python (or with Flow/Graph/Agent when you want orchestration). Less abstraction between you and the call, more predictability.

Overview — where each thing lives

What you did in LangChainIn jangada
init_chat_model / ChatOpenAI, ChatAnthropic, …LLM("provider", "model") (Getting started)
ChatPromptTemplate + chain.invoke{{ }} template directly in complete()
with_structured_output(Model)parse(prompt, Model) → .parsed (Structured output)
model.bind_tools([...]) + manual loopcomplete(tools=[...]) or Agent (Tools)
AgentExecutor / create_agent (langgraph)Agent / Squad (Agents)
RecursiveCharacterTextSplitter + vectorstore + as_retrieverRAG + vector_store(url) (RAG)
PyPDFLoader, Docx2txtLoader, …files=[Document(...)] (Documents)
langchain-mcp-adaptersmcp_servers= or MCPClient/run_agent (MCP)
chain.stream(...)stream() / astream() (Streaming)
.with_retry() / .with_fallbacks([...])retry_on=/max_retries= + with_fallback() (Retry and fallback)
set_llm_cache(...)LLM(..., cache=ExactCache()/SemanticCache()) (Cache)
RunnableSequence (LCEL a → b → c)Flow (sequential) / Graph (conditional) (Flows)
get_openai_callback() / LangSmithCompletion.cost/.usage + observability_session (Cost, Observability)

Initialize and switch providers

In LangChain you pick the class (ChatOpenAI, ChatAnthropic) or use init_chat_model("provider:model"). jangada has one class and the switch is the first two arguments:

from jangada_ai import LLM

llm = LLM("anthropic", "claude-sonnet-4-6")
llm = LLM("openai",    "gpt-5")          # switch provider = switch 2 args
resp = llm.complete("Explain rafts in one sentence.")
print(resp.text, resp.cost)

Differential. Parameters are canonical (temperature, max_tokens, top_p, top_k, stop, seed) and each adapter translates to the native name, discarding what the provider doesn't support — you don't rewrite max_tokens → max_completion_tokens per model. Per-model quirks (gpt-5 without temperature, Gemini thinking) are handled by profiles, not if model ==. See Parameters and profiles.

Prompts and templates

In LangChain the prompt is an object (ChatPromptTemplate.from_messages([...])) plugged into a chain. In jangada the {{ }} template lives inside the prompt itself and variables come as keyword args:

llm.complete("Summarize {{topic}} in {{n}} sentences.", topic="MCP", n=2)
# separate system, history and params in the same call:
llm.complete(
    "Classify: {{text}}",
    system="You are a strict classifier.",
    history=[...],                 # previous turns (list[Message])
    params={"temperature": 0},
    text="...",
)

Differential. The engine only interpolates {{ identifier }}; plain { } (literal JSON in the prompt) is never touched, and with no kwargs the prompt passes through intact. No PromptTemplate/partial/format_messages — it's string + kwargs.

Chaining calls

LCEL chains with the pipe operator (prompt | model | parser). jangada gives you two explicit options, depending on the flow:

  • Flow for a linear pipeline (each step becomes a {{ }} variable for the next; a step can come out typed with schema=):
from jangada_ai import Flow

flow = (
    Flow(llm)
    .step("summary", "Summarize:\n{{text}}")
    .step("title", "Give a title for:\n{{summary}}", schema=Title)
)
r = flow.run(text="...")
print(r.completions["title"].parsed, r.cost)   # cost/usage aggregated
  • Graph when there is conditional or parallel routing (what you'd reach for langgraph for in the LangChain ecosystem).

Differential. No invisible Runnable: Flow/Graph aggregate usage/cost automatically and the step-by-step shows up in debug. For simple logic, a plain Python for/if already does the job — no composition abstraction needed. See Flows.

Structured output

LangChain: model.with_structured_output(Model). jangada: parse().

from pydantic import BaseModel

class Person(BaseModel):
    name: str
    age: int

resp = llm.parse("Extract: John is 30.", Person)
person = resp.parsed           # ← validated Pydantic instance
print(person.name, resp.cost)  # parse() returns a Completion; the object is in .parsed

Differentials. (1) It works the same across all providers: OpenAI uses the native helper, Gemini uses response_schema, Anthropic uses tool-forcing, and Groq falls back to JSON object mode automatically when the model doesn't support json_schema — you don't pick a method=. (2) If the response is truncated by max_tokens, the library raises errors.TruncatedError ("raise max_tokens") before Pydantic blows up with a confusing cut-off-JSON error. See Structured output.

Tools (function calling)

In LangChain you do model.bind_tools([...]) and write the tool-calling loop (or use an agent). In jangada the low-level path is complete(tools=[...]); the high-level one is Agent, which runs the loop for you.

def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

# low level: you execute and resend
resp = llm.complete("What's 2+3?", tools=[add], tool_choice="auto")
for call in resp.tool_calls:
    out = add(**call.args)
    # resend via comp.assistant_message() + Message.tool_results(call.result(out))

Differential. Plain Python functions become a Tool automatically — the docstring is the description the model reads. The normalization of tool_calls/tool_results is the same across all four providers (including the Gemini detail of matching a result by name). See Tools.

Agents and teams

In modern LangChain, agents are built with langgraph (create_agent) or the older AgentExecutor. In jangada it's Agent (plain-Python composition over the tool loop) and Squad for teams:

from jangada_ai import Agent, Squad

researcher = Agent(llm, role="Researcher", goal="gather facts", tools=[search])
writer     = Agent(llm, role="Writer", goal="write the summary")
resp = Squad([researcher, writer]).run("Write a briefing on X.")

Differential. No graph or implicit state: Squad is sequential (handoff: one's output becomes the next's context) or hierarchical (a manager delegates), and aggregates cost/usage. Long-term memory is RAGMemory (reuses the RAG module). Decomposing a goal into tasks is plan(llm, goal). See Agents.

RAG

LangChain: text splitter + vectorstore + as_retriever() + a QA chain. jangada's RAG orchestrates ingestion and search, and vector_store(url) picks the backend from the connection string:

from jangada_ai import LLM, RAG, Document
from jangada_ai.rag import vector_store

emb = LLM("openai", "text-embedding-3-small")
rag = RAG(emb, vector_store("memory"), chat=LLM("openai", "gpt-5"))
rag.add_document(Document(pdf_bytes, name="manual.pdf"))   # chunk + embed + store
hits = rag.search("how do I X?", k=4, mode="hybrid")       # vector + BM25 (RRF)

Differentials. Built-in hybrid search via RRF (mode="hybrid", tune with alpha/weights); vector_store("memory" | "postgres://…" | "mongodb://…") detects the backend on its own; chunking and task=document/query (which some providers distinguish) are handled. DB deps are lazy imports. See RAG.

Documents (pdf, docx, csv, xlsx)

In LangChain you pick a loader per format (PyPDFLoader, Docx2txtLoader, CSVLoader, …). In jangada you pass files= and the library resolves the format:

from jangada_ai import Document

llm.parse("Extract the items.", Order, files=[Document(xlsx_bytes, name="o.xlsx")])

Differential. By default (mode="auto") jangada extracts text locally (cheaper, works on non-vision models); it only uses vision if you force mode="vision" or the PDF is scanned. At the client boundary each file becomes a TextPart/ImagePart — the adapters never see a "document format". See Documents.

Vision and audio

Images come in via images= (path, bytes or ImagePart); to label each image, pass tuples ("label", img):

llm.parse("Compare.", Comparison, images=[("front", "a.jpg"), ("back", "b.jpg")])

Transcription is transcribe/atranscribe (OpenAI, Groq, Gemini):

from jangada_ai import Audio
text = llm.transcribe(Audio.from_path("meeting.mp3")).text

Differential. Everything via normalized types (ImagePart/AudioPart carry only bytes), the same across providers. See Vision and Audio. There's also provider-agnostic object detection (detect_objects) — no direct LangChain equivalent.

MCP

In LangChain you use langchain-mcp-adapters. jangada has two paths: remote MCP by URL via mcp_servers= in complete, and a first-party client with an agent loop:

from jangada_ai import MCPClient, run_agent

async with MCPClient("https://server/mcp", headers={...}) as mcp:
    resp = await run_agent(llm, "Use the tools to…", client=mcp)

Differential. The first-party client covers all MCP primitives (tools, resources, prompts) and the client features (roots, sampling). See MCP.

Streaming

for token in llm.stream("Write a paragraph about rafts."):
    print(token, end="", flush=True)
# async: async for token in llm.astream(...)

Differential. stream yields strings (text tokens), not chunk objects to assemble. See Streaming.

Retry and fallback

LangChain: .with_retry() and .with_fallbacks([...]). jangada's retry is LLM config and the fallback is chained:

llm = LLM("anthropic", "claude-sonnet-4-6", max_retries=3).with_fallback(
    LLM("openai", "gpt-5"),
    LLM("groq",   "llama-3.3-70b-versatile"),
)
resp = llm.complete("...")
print(resp.provider)   # who actually answered

Decisive differential. Failover decides by typed error (LLMError: rate limit, timeout, 5xx, 404), not by string-matching the error message ("503" in str(e)). Auth/bad-request errors do not trigger fallback (switching providers won't help). See Retry and fallback and Errors.

Cache

LangChain: global set_llm_cache(...). jangada's cache is per LLM, exact or semantic:

from jangada_ai import LLM, ExactCache, SemanticCache

llm = LLM("openai", "gpt-5", cache=ExactCache(max_size=512, ttl=3600))
# semantic (catches paraphrases) needs an embedder:
emb = LLM("openai", "text-embedding-3-small")
llm = LLM("openai", "gpt-5", cache=SemanticCache(emb, threshold=0.45))

See Cache.

Guardrails

LangChain has no native guardrail (you use guardrails-ai or your own logic). In jangada ScopeGuard keeps the conversation in scope, with a blocklist (regex) + an LLM judge:

from jangada_ai import LLM, ScopeGuard

guard = ScopeGuard("product X support", judge=llm, check="input", raise_on_block=True)
agent = LLM("openai", "gpt-5", guardrails=[guard])

See Guardrails.

Cost and observability

In LangChain you use get_openai_callback() or LangSmith. In jangada every response already carries usage and cost, and there's a call grouper:

from jangada_ai import observability_session

with observability_session(name="extraction", metadata={"doc": "inv-123"}):
    a = llm.parse("...", Invoice, files=[...])
    b = llm.complete("...")
# a.usage, a.cost available on each response; Flow/Graph/Squad aggregate

Differential. Cost is a per-token estimate on every call (tune with register_price; audio per minute with register_audio_price). It's optional, lightweight observability — you don't need an external service to see cost/tokens. See Cost and Observability.

jangada differentials (summary)

  • Normalized types at the boundary (Message/Completion): no native SDK object leaking — it stays in Completion.raw if you want it.
  • Canonical params + per-model profiles: no if model == for quirks.
  • Transparent Gemini thinking: pass thinking_budget/thinking_level and the library adapts to the version. See Gemini.
  • Uniform structured output across providers (with automatic Groq fallback).
  • Fallback by typed error, not by error string.
  • Lazy imports + single dependency: import jangada_ai works with no SDK installed; each provider is an optional extra.
  • PT-BR in error messages, docstrings and docs (this project's default).

Dependencies — what changes

Where you had several packages (langchain, langchain-core, langchain-openai, langchain-anthropic, langchain-google-genai, langgraph, langsmith, …), you now have one:

pip install "jangada-ai[all]"          # all providers + documents
pip install "jangada-ai[all,rag,mcp]"  # + RAG + MCP

Less transitive surface to version and less breakage when an SDK changes. Start with the installation and move on to the Tutorials.

On this page