# LangChain vs LangGraph: Why Keep Both Integrations

## Executive Summary

**Keep both integrations.** They serve different but complementary purposes and are designed to coexist without conflict.

## Key Points

### 1. LangGraph Requires LangChain

LangGraph is **built on top of** LangChain, not a replacement:

```toml
# From LangGraph's pyproject.toml
dependencies = [
    "langchain-core>=0.1",     # REQUIRED
    "langgraph-checkpoint>=2.1.0,<4.0.0",
    "langgraph-sdk>=0.3.0,<0.4.0",
    # ...
]
```

LangGraph uses LangChain's:
- LLM integrations (OpenAI, Anthropic, etc.)
- Tool interfaces
- Document loaders
- **Callback system** (which our integrations hook into)
- Runnables architecture

### 2. Different Use Cases

| Aspect | LangChain | LangGraph |
|--------|-----------|-----------|
| **Purpose** | LLM/chain composition | Stateful workflows |
| **Complexity** | Simple → Medium | Medium → Complex |
| **State** | Implicit (in chain) | Explicit (StateGraph) |
| **Flow** | Linear/branching | Cyclical/iterative |
| **Persistence** | None | Checkpointing |
| **Best For** | RAG, chatbots, tools | Multi-step agents, workflows |

### 3. Real User Scenarios

#### Scenario A: Pure LangChain User
```python
# User only uses LangChain
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate

llm = ChatOpenAI()
chain = PromptTemplate(...) | llm | parser

# ✅ LangChain integration captures this
# ❌ LangGraph integration not needed
```

#### Scenario B: Pure LangGraph User
```python
# User only uses LangGraph
from langgraph.graph import StateGraph

graph = StateGraph(State)
graph.add_node("process", process_func)
compiled = graph.compile()

# ✅ LangGraph integration captures graph structure
# ✅ LangChain integration captures nested LLM calls
#    (LangGraph nodes often use LangChain LLMs internally)
```

#### Scenario C: Mixed Usage (Most Common)
```python
# User mixes both
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph

# Some workflows are simple LangChain
llm = ChatOpenAI()
simple_chain = prompt | llm | parser

# Some workflows need LangGraph's state management
def complex_node(state):
    # Uses LangChain LLM inside LangGraph node
    result = llm.invoke(state["input"])
    return {"output": result.content}

graph = StateGraph(State)
graph.add_node("complex", complex_node)

# ✅ LangChain integration: simple_chain, nested LLM in node
# ✅ LangGraph integration: graph structure, node execution
```

## What Each Integration Captures

### LangChain Integration

**Captures:**
- ✅ `llm.call.start` / `llm.call.end` - LLM invocations
- ✅ `chain.start` / `chain.end` - Chain executions
- ✅ `tool.call.start` / `tool.call.end` - Tool invocations
- ✅ `retriever.start` / `retriever.end` - RAG operations
- ✅ `agent.action` / `agent.finish` - LangChain agent actions

**Use cases:**
- Direct LangChain usage
- Nested LangChain calls within other frameworks
- RAG pipelines
- Simple chatbots

### LangGraph Integration

**Captures:**
- ✅ `graph.start` / `graph.end` - Graph execution
- ✅ `agent.start` / `agent.end` - Node execution (nodes → agents)
- ✅ `checkpoint.save` / `checkpoint.load` - State persistence
- ✅ `task.start` / `task.end` - Parallel execution
- ✅ Nested LLM/tool calls (via inherited LangChain callbacks)

**Use cases:**
- StateGraph workflows
- Multi-step agent systems
- Stateful applications
- Workflows with checkpointing

## Preventing Duplicate Events

### The Problem
When both handlers are active, they could both emit events for the same callback.

### The Solution
**Handler specificity** - Each handler only processes what it's designed for:

```python
# In LangGraph handler
def _get_chain_type(self, serialized):
    # Check if this is LangGraph-specific
    if "Pregel" in str(id_list) or "StateGraph" in str(id_list):
        return "graph"
    elif is_langgraph_node(serialized):
        return "node"
    else:
        return "other"  # ← Let LangChain handler process this

def on_chain_start(self, ...):
    chain_type = self._get_chain_type(serialized)
    if chain_type == "other":
        return  # ← Skip non-LangGraph chains
    # ... process LangGraph-specific chains
```

### Result: Clean Hierarchy

```
graph.start (LangGraph)              ← Graph-level
  ├─ agent.start node_a (LangGraph)  ← Node-level
  │   ├─ llm.call.start (LangChain)  ← Nested LLM
  │   └─ llm.call.end (LangChain)    ← Nested LLM
  ├─ agent.end node_a (LangGraph)    ← Node-level
  ├─ chain.start simple (LangChain)  ← Direct LangChain usage
  └─ chain.end simple (LangChain)    ← Direct LangChain usage
graph.end (LangGraph)                ← Graph-level
```

## Auto-Instrumentation Strategy

When `arzule_ingest.init()` is called:

```python
def init(auto_instrument=True):
    # ...
    
    if auto_instrument:
        # 1. Check and instrument LangChain
        if _check_langchain_available():
            from .langchain.install import instrument_langchain
            instrument_langchain()
        
        # 2. Check and instrument LangGraph
        if _check_langgraph_available():
            from .langgraph.install import instrument_langgraph
            instrument_langgraph()
        
        # Both can coexist without conflicts
```

## When to Disable Each

You can selectively disable integrations:

```python
# Disable LangChain, keep LangGraph (rare)
from arzule_ingest.langchain import instrument_langchain
handler_lc = instrument_langchain()
# Don't pass handler_lc to config

# Disable LangGraph, keep LangChain (if using pure LangChain)
from arzule_ingest.langgraph import instrument_langgraph
handler_lg = instrument_langgraph()
# Don't pass handler_lg to config

# Use both (recommended)
compiled.invoke(input, config={"callbacks": [handler_lc, handler_lg]})
```

## Performance Considerations

### With Both Handlers Active

- **Overhead**: Minimal - callbacks only fire once per event
- **Duplication**: None - handlers check chain type
- **Memory**: ~1KB per handler instance
- **Latency**: <1ms per callback

### Recommendation

**Enable both by default** unless you're certain you only use one framework.

## Migration Strategy

If you're migrating from LangChain to LangGraph:

1. **Phase 1**: Keep both integrations active
2. **Phase 2**: Gradually convert LangChain chains to LangGraph nodes
3. **Phase 3**: Keep both - you'll likely still use LangChain LLMs within nodes
4. **Result**: Never fully remove LangChain integration

## Decision Matrix

| Your Application | LangChain | LangGraph | Both |
|------------------|-----------|-----------|------|
| Uses only LangChain | ✅ | ❌ | Optional |
| Uses only LangGraph | Recommended* | ✅ | ✅ |
| Uses both | Required | Required | ✅ |
| Unknown/mixed | ✅ | ✅ | ✅ |

\* Even if using "only" LangGraph, you likely use LangChain LLMs internally.

## Conclusion

**Do NOT remove the LangChain integration.** Here's why:

1. ✅ LangGraph is built on LangChain
2. ✅ Users may use LangChain without LangGraph
3. ✅ LangGraph nodes often use LangChain components
4. ✅ No conflict - handlers are designed to coexist
5. ✅ Minimal overhead
6. ✅ Better coverage of mixed applications

**Recommended action**: Keep both, ensure proper chain type detection (already implemented).

