# Langfuse SDK

Instrument your assistant with Langfuse and let Brizz ingest the OTel spans.

If you already instrument your assistant with the [Langfuse Python SDK](https://langfuse.com/), Brizz ingests its OpenTelemetry spans directly. Sessions, generations, tool calls, and per-turn context all show up in the Brizz dashboard without you having to call Brizz's session API (`start_session` / `set_input` / `set_output`).

## Quick start

Let Langfuse own AI-library instrumentation and have Brizz ingest its OTel spans. Brizz events and sessions still work the same way.

```python
import os
from brizz import Brizz

Brizz.initialize(
    api_key=os.getenv("BRIZZ_API_KEY"),
    app_name="my-app",
    allowed_instrumentations=[], # Disable auto-instrumentation
)

# Langfuse reads LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_HOST from env.
from langfuse import get_client
langfuse = get_client()
```

Group related LLM calls under a Langfuse session. Brizz picks up `session.id` from Langfuse automatically.

```python
from langfuse import get_client, propagate_attributes
# Drop-in replacement: Langfuse's OpenAI wrapper creates observations automatically.
from langfuse.openai import openai

langfuse = get_client()

with langfuse.start_as_current_observation(as_type="span", name="chat") as span:
    with propagate_attributes(session_id="chat-session-123", user_id="user-123"):
        response = openai.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": "Hello"}],
        )

# Alternative — wrap with Brizz's session API instead:
# with start_session("chat-session-123"):
#     response = openai.chat.completions.create(...)
```

## Per-turn context

Brizz renders a collapsible **Context** panel on both the user and the assistant message in the conversation view. Populate the user side with a reserved `brizz_user_context` key and the assistant side with `brizz_assistant_context`. Brizz reads only those two keys — anything else in Langfuse metadata stays out of the Context panel.

You can attach each bag two ways, and they compose:

- **Trace-level** — `propagate_attributes(metadata=…)`. Applies to the turn produced inside the block.
- **Per-observation** — `start_as_current_observation(metadata=…)` (or `span.update(metadata=…)`). Applies to that observation.

When the same key is set both ways, the **per-observation value wins** — per key, so trace-only keys still merge in.

Langfuse's metadata bag accepts string values only, so JSON-encode each bag. Set the trace-level context with `propagate_attributes`:

```python
import json
from langfuse import get_client, propagate_attributes

langfuse = get_client()

with propagate_attributes(
    session_id="chat-session-123",
    user_id="user-42",
    metadata={
        "brizz_user_context": json.dumps({
            "ui_scope_id":    scope_id,
            "ui_origin_view": view,
            "ui_model_type":  model_type,
        }),
        "brizz_assistant_context": json.dumps({
            "answer_source": "knowledge_base",
            "confidence":    "high",
        }),
    },
):
    response = await graph.ainvoke(state)
```

To override (or add) context for a single observation, pass the same keys on `start_as_current_observation`. Any shared key wins over the trace-level value:

```python
with langfuse.start_as_current_observation(
    as_type="span",
    name="chat",
    metadata={
        "brizz_user_context": json.dumps({"ui_scope_id": scope_id}),
    },
) as span:
    response = await graph.ainvoke(state)
    # Assistant outcome is known only after the model responds — set it on the
    # observation once you have it.
    span.update(metadata={
        "brizz_assistant_context": json.dumps({"answer_source": "live_tool"}),
    })
```

Brizz parses each JSON object server-side and renders its key/values on the Context panel — `brizz_user_context` on the user message, `brizz_assistant_context` on the assistant message produced inside that block. Use a fresh scope per turn — the contents are per-turn, not per-session. An empty value is ignored (it never blanks out a value set the other way).

Identity fields like `user_id`, `user_email`, or `organization_id` — placed at the top level of Langfuse metadata (alongside `brizz_user_context`, not inside it) — are preserved on the underlying span but kept out of the Context panel so it stays focused on the signals you choose to surface.

## Complete example

A single file you can copy and run with `python main.py`. Required env: `BRIZZ_API_KEY`, `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_HOST` (optional — defaults to Langfuse Cloud), `OPENAI_API_KEY`.

```python
# main.py
from dotenv import load_dotenv
load_dotenv()

import json
import os
from brizz import Brizz, emit_event

# allowed_instrumentations=[] tells Brizz to skip AI-library auto-instrumentation
# and only ingest Langfuse's OTel spans (which already cover OpenAI / LangChain / etc).
Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    app_name="my-app",
    environment=os.getenv("APP_ENV", "development"),
    allowed_instrumentations=[],
)

# Langfuse's OpenAI drop-in creates observations for every call automatically.
from langfuse import get_client, propagate_attributes
from langfuse.openai import openai

langfuse = get_client()


def run_turn(session_id: str, user_id: str, user_message: str, scope_id: str) -> str:
    with langfuse.start_as_current_observation(as_type="span", name="chat") as span:
        # Langfuse metadata is string-only — JSON-encode each Brizz Context panel
        # payload. user_id / session_id stay top-level on propagate_attributes.
        with propagate_attributes(
            session_id=session_id,
            user_id=user_id,
            metadata={
                # User-turn context (trace-level): known before the model runs.
                "brizz_user_context": json.dumps({
                    "ui_scope_id": scope_id,
                    "ui_origin_view": "support_inbox",
                }),
            },
        ):
            response = openai.chat.completions.create(
                model="gpt-4o-mini",
                messages=[{"role": "user", "content": user_message}],
            )
            reply = response.choices[0].message.content

        # Assistant-turn context (per-observation): known only after the model
        # responds. A shared key here would win over any trace-level value.
        span.update(metadata={
            "brizz_assistant_context": json.dumps({
                "answer_source": "billing_kb",
                "finish_reason": response.choices[0].finish_reason,
            }),
        })

    # Brizz custom events still work — the same session_id is honored.
    # Map "feedback.positive" / "feedback.negative" to system events in
    # Org Settings -> Event so it powers filters and the Overview chart.
    emit_event(
        "feedback.positive",
        attributes={"category": "helpfulness"},
        body={"comment": "Exactly what I needed!", "context": "billing-assistant"},
    )
    return reply


if __name__ == "__main__":
    print(run_turn("chat-session-123", "user-42", "Why is my bill high?", "scope-9"))
```

**Gotchas**

- `allowed_instrumentations=[]` is what keeps Brizz and Langfuse from double-instrumenting the same OpenAI / LangChain call.
- Langfuse metadata (`propagate_attributes` or observation) accepts strings only — JSON-encode each context bag (`brizz_user_context`, `brizz_assistant_context`).
- Open a fresh `propagate_attributes` scope per user turn; the Context panel is per-turn, not per-session.
- `user_id` (and `user_email`, `organization_id`) belong at the top level of `propagate_attributes`, not inside `brizz_user_context`.

## See also

- [Python SDK](/docs/sdks/python.md) — full SDK reference.
- [Sessions](/docs/instrument/sessions.md) — session capture patterns.
- [Identify users](/docs/instrument/identify-users.md) — propagating identity into spans.
