Metadata-Version: 2.4
Name: fiddler-langchain
Version: 0.1.1
Summary: Fiddler SDK for instrumenting LangChain V1 agents with OpenTelemetry
Author-email: Fiddler AI <support@fiddler.ai>
License-Expression: Apache-2.0
Project-URL: Homepage, https://fiddler.ai
Project-URL: Documentation, https://docs.fiddler.ai
Project-URL: Repository, https://github.com/fiddler-labs/fiddler-sdk
Project-URL: Issues, https://github.com/fiddler-labs/fiddler-sdk/issues
Keywords: fiddler,ai,genai,llm,monitoring,observability,instrumentation,langchain,opentelemetry
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Monitoring
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: fiddler-otel<1.0.0,>=0.1.0
Requires-Dist: langchain>=1.0.0
Requires-Dist: opentelemetry-instrumentation>=0.56b0
Requires-Dist: wrapt>=1.14.0
Provides-Extra: dev
Requires-Dist: setuptools<82.0.0,>=61.0; extra == "dev"
Requires-Dist: pytest>=8.3.5; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
Requires-Dist: pytest-cov>=6.1.1; extra == "dev"
Requires-Dist: mypy>=1.16.1; extra == "dev"
Requires-Dist: ruff>=0.12.0; extra == "dev"
Requires-Dist: langchain-openai>=0.2.14; extra == "dev"
Requires-Dist: langchain-community>=0.3.27; extra == "dev"

# fiddler-langchain

Fiddler observability for **LangChain V1** agents built with `langchain.agents.create_agent`.

## Installation

```bash
pip install fiddler-langchain
```

## Quick Start

Call `FiddlerLangChainInstrumentor.instrument()` once after creating your `FiddlerClient`. Every agent created with `langchain.agents.create_agent()` is then traced automatically. Use the optional `name` argument to label agents in traces; if omitted, the agent name is left empty.

```python
import langchain.agents
from fiddler_otel import FiddlerClient
from fiddler_langchain import FiddlerLangChainInstrumentor

client = FiddlerClient(
    api_key="YOUR_API_KEY",
    application_id="YOUR_APPLICATION_ID",
    url="https://your-instance.fiddler.ai",
)

instrumentor = FiddlerLangChainInstrumentor(client=client)
instrumentor.instrument()

agent = langchain.agents.create_agent(
    model="openai:gpt-4o-mini",
    tools=[...],
    name="my_agent",
)

result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})
```

**Alternative (manual middleware):** You can instead pass `middleware=[FiddlerAgentMiddleware(client=client, agent_name="my_agent")]` to each `create_agent()` call and skip the instrumentor. Use the instrumentor when you want a single `instrument()` to trace all agents.

## Trace Hierarchy

Each invocation produces a clean, flat hierarchy with no noisy Chain wrappers:

```
[Span] my_agent          (Agent root - TYPE=agent)
  └── [Span] gpt-4o-mini (LLM call - TYPE=llm)
  └── [Span] hotel_search (Tool call - TYPE=tool)
  └── [Span] gpt-4o-mini (LLM call - TYPE=llm)
```

> **Note on `span_type=agent` for the root span:** The root span uses `span_type=agent`
> to accurately represent that it is an agent invocation. It carries `agent_name`,
> `agent_id`, and `conversation_id` but has empty LLM and tool fields (`model_name`,
> `llm_output`, `tool_name`, etc.) because it is a container span for the full agent
> lifecycle - not an LLM or tool call itself. The legacy `chain` span type is deprecated
> and no longer used for agent root spans.

## Multi-turn Conversations

```python
from fiddler_langchain import set_conversation_id
import uuid

set_conversation_id(str(uuid.uuid4()))
agent.invoke({"messages": [...]})
```

## LLM Context

Attach contextual metadata to LLM spans by calling `set_llm_context` before the agent runs.
The instrumentation reads this value from the model's metadata at invocation time.

```python
import langchain.agents
from langchain_openai import ChatOpenAI
from fiddler_langchain import FiddlerLangChainInstrumentor, set_llm_context
from fiddler_otel import FiddlerClient

client = FiddlerClient(api_key="...", application_id="...", url="...")
FiddlerLangChainInstrumentor(client=client).instrument()

model = ChatOpenAI(model="gpt-4o-mini")
set_llm_context(model, "User preference: concise answers")

agent = langchain.agents.create_agent(model=model, tools=[...], name="my_agent")
```

## Retriever Instrumentation

The LangChain V1 middleware API does not expose a dedicated retriever hook. Following the
same convention used in `fiddler-langgraph`, **retrievers are treated as tools**.

Wrap your retriever with `@tool` (or `create_retriever_tool`) and pass it to `create_agent`.
The instrumentation's tool hook captures the retriever call automatically as a
`TYPE=tool` span - with the query as `tool_input` and the retrieved documents as `tool_output`.

```python
import langchain.agents
from langchain_core.tools import tool
from fiddler_langchain import FiddlerLangChainInstrumentor
from fiddler_otel import FiddlerClient

client = FiddlerClient(api_key="...", application_id="...", url="...")
FiddlerLangChainInstrumentor(client=client).instrument()

retriever = vector_store.as_retriever()

@tool
def search_docs(query: str) -> str:
    """Search company documents for relevant information."""
    return str(retriever.invoke(query))

agent = langchain.agents.create_agent(
    model="openai:gpt-4o-mini",
    tools=[search_docs, ...],
    name="rag_agent",
)
```

The resulting trace looks like:

```
[Span] rag_agent           (Agent root - TYPE=agent)
  └── [Span] gpt-4o-mini   (LLM call - TYPE=llm)
  └── [Span] search_docs   (Retriever as Tool - TYPE=tool)
  └── [Span] gpt-4o-mini   (LLM call - TYPE=llm)
```

## Multi-Agent Setup

With the instrumentor, a single `instrument()` call patches `create_agent` so every agent is traced. Pass `name='...'` to each `create_agent()` to label agents in traces. When a **supervisor** delegates work to sub-agents via tools, the entire flow now appears as a **single trace** for that user request: the supervisor is the root span, delegation tools are tool spans under it, and each sub-agent root span is a child of the corresponding delegation tool span. All spans share the same `conversation_id`.

```python
import uuid
import langchain.agents
from langchain_openai import ChatOpenAI
from fiddler_langchain import FiddlerLangChainInstrumentor, set_conversation_id
from fiddler_otel import FiddlerClient

client = FiddlerClient(api_key="...", application_id="...", url="...")
instrumentor = FiddlerLangChainInstrumentor(client=client)
instrumentor.instrument()

# Sub-agents - invoked via supervisor tools
flight_agent = langchain.agents.create_agent(
    model=ChatOpenAI(), tools=[book_flight], name="flight_assistant"
)
hotel_agent = langchain.agents.create_agent(
    model=ChatOpenAI(), tools=[search_hotel, book_hotel], name="hotel_assistant"
)

# Supervisor - delegates to sub-agents via @tool wrappers
supervisor = langchain.agents.create_agent(
    model=ChatOpenAI(),
    tools=[delegate_to_flight_assistant, delegate_to_hotel_assistant],
    name="supervisor",
)

# Link the whole flow under one conversation + one trace
set_conversation_id(str(uuid.uuid4()))
supervisor.invoke({"messages": [{"role": "user", "content": "Book a flight and a hotel."}]})
```

The resulting trace looks like:

```
[Span] supervisor                          (root - TYPE=agent)
  ├── [Span] gpt-4o-mini                   (LLM  - TYPE=llm)
  ├── [Span] delegate_to_flight_assistant  (Tool - TYPE=tool)
  │     └── [Span] flight_assistant        (Agent - TYPE=agent)
  │           ├── [Span] gpt-4o-mini       (LLM  - TYPE=llm)
  │           └── [Span] book_flight       (Tool - TYPE=tool)
  └── [Span] delegate_to_hotel_assistant   (Tool - TYPE=tool)
        └── [Span] hotel_assistant         (Agent - TYPE=agent)
              ├── [Span] gpt-4o-mini       (LLM  - TYPE=llm)
              ├── [Span] search_hotel      (Tool - TYPE=tool, retriever-as-tool)
              └── [Span] book_hotel        (Tool - TYPE=tool)
```

## Local JSONL Capture

To capture all spans to a local JSONL file without sending to Fiddler (useful for debugging):

```python
client = FiddlerClient(
    api_key="...",
    application_id="...",
    url="...",
    jsonl_capture_enabled=True,
    jsonl_file_path="trace_data.jsonl",
)
```

Or via environment variables:

```bash
FIDDLER_JSONL_ENABLED=true \
FIDDLER_JSONL_FILE=trace_data.jsonl \
python my_agent.py
```

Each line in the output file is a JSON object containing all span attributes:
`trace_id`, `span_id`, `parent_span_id`, `span_type`, `agent_name`, `conversation_id`,
`model_name`, `model_provider`, `llm_input_system`, `llm_input_user`, `llm_output`,
`llm_context`, `llm_token_count_input/output/total`, `gen_ai_input_messages`,
`gen_ai_output_messages`, `tool_name`, `tool_input`, `tool_output`, `tool_definitions`.

## Async Agents

The instrumentation fully supports async agents via `awrap_model_call` and
`awrap_tool_call`. Use `agent.ainvoke()` instead of `agent.invoke()` - no additional
configuration is needed:

```python
import asyncio
import langchain.agents
from langchain_openai import ChatOpenAI
from fiddler_langchain import FiddlerLangChainInstrumentor
from fiddler_otel import FiddlerClient

client = FiddlerClient(api_key="...", application_id="...", url="...")
FiddlerLangChainInstrumentor(client=client).instrument()

agent = langchain.agents.create_agent(
    model=ChatOpenAI(model="gpt-4o-mini"),
    tools=[...],
    name="my_agent",
)

async def main():
    result = await agent.ainvoke({"messages": [{"role": "user", "content": "Hello!"}]})
    print(result)

asyncio.run(main())
```

The instrumentation automatically uses the async lifecycle hooks (`awrap_model_call`,
`awrap_tool_call`) when the agent is invoked asynchronously, producing the same span
hierarchy as the sync path.

## Error Handling

If an LLM call or tool call raises an exception, the instrumentation catches it, marks the
corresponding span with `StatusCode.ERROR`, re-raises the exception so normal error
handling in your application is unaffected, and still cleanly closes the root agent span.

```python
try:
    result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})
except Exception as e:
    # The failing LLM or tool span is already marked ERROR in Fiddler
    # The root agent span is also closed - no dangling spans
    raise
```

This means:
- **Partial traces are never lost** — all spans up to the point of failure are recorded
- **The failing span** carries `status_code=ERROR` and the exception message
- **The root agent span** is always closed, regardless of whether the invocation succeeded or failed

## Relationship to fiddler-langgraph

| Package | Framework | Instrumentation |
|---|---|---|
| `fiddler-langgraph` | LangGraph (`StateGraph.compile()`) | Callback handler |
| `fiddler-langchain` | LangChain V1 (`create_agent`) | `FiddlerLangChainInstrumentor` (auto) or `FiddlerAgentMiddleware` (manual) |
