Session-Based Observability for Multi-Turn Conversations

Group every span from a multi-turn chatbot by session and user ID so conversations appear as a single, filterable unit in the FutureAGI Tracing dashboard.

📝
TL;DR

Tag every LLM span with user and session IDs so multi-turn conversations appear as grouped, filterable sessions in the FutureAGI Tracing dashboard.

Open in ColabGitHub
TimeDifficultyPackage
15 minBeginnerfi-instrumentation-otel
Prerequisites

Install

pip install fi-instrumentation-otel traceai-openai openai
export FI_API_KEY="your-api-key"
export FI_SECRET_KEY="your-secret-key"
export OPENAI_API_KEY="your-openai-api-key"

What is session-based observability?

When a user has a multi-turn conversation with your chatbot, each message generates a separate LLM span. Without session context, those spans appear as unrelated entries in Tracing. using_session() and using_user() attach a shared session.id and user.id to every span created inside the context block. All turns from one conversation are grouped together in the Sessions tab and viewable as a single conversation thread.


Tutorial

Register the tracer and instrument OpenAI

register() creates a tracer provider connected to FutureAGI. OpenAIInstrumentor patches the OpenAI client so every chat.completions.create call is captured automatically: model name, messages, token counts, and latency. No further code changes are needed.

import os
from fi_instrumentation import register
from fi_instrumentation.fi_types import ProjectType
from traceai_openai import OpenAIInstrumentor
from openai import OpenAI

# Connect to FutureAGI tracing
trace_provider = register(
    project_type=ProjectType.OBSERVE,
    project_name="chatbot-session-demo",
)

# Patch OpenAI so every call is traced automatically
OpenAIInstrumentor().instrument(tracer_provider=trace_provider)

client = OpenAI()

Expected output:

🔭 OpenTelemetry Tracing Details 🔭
|  FI Project: chatbot-session-demo
|  FI Project Type: observe
|  FI Project Version Name: DEFAULT_PROJECT_VERSION_NAME
|  Span Processor: BatchSpanProcessor
|  Collector Endpoint: https://...
|  Transport: HTTP
|  Transport Headers: {'X-Api-Key': '****', 'X-Secret-Key': '****'}
|  Eval Tags: []

Tag a single request with user and session context

Wrap any OpenAI call with using_user() and using_session() context managers. Every span created inside the block (including those generated by OpenAIInstrumentor) automatically inherits the user.id and session.id attributes.

from fi_instrumentation import using_user, using_session

user_id = "user-7f3a2b"
session_id = "session-c91d4e"

with using_user(user_id), using_session(session_id):
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": "Hello, what can you help me with?"}],
    )
    print(response.choices[0].message.content)

Expected output:

I can help you with a wide range of topics — answering questions, drafting text,
explaining concepts, writing code, and much more. What would you like to explore?

Go to app.futureagi.comTracing (left sidebar under OBSERVE). The span appears with user.id = user-7f3a2b and session.id = session-c91d4e visible in the attributes panel.

Simulate a multi-turn conversation

This is the core pattern — a conversation loop where each turn calls OpenAI inside the same using_user and using_session block. All three turns share identical user.id and session.id values, so they appear grouped in the dashboard.

from fi_instrumentation import using_user, using_session

def run_conversation(user_id: str, session_id: str) -> None:
    """Run a 3-turn conversation. All spans share the same user and session IDs."""

    turns = [
        "What is photosynthesis?",
        "How does it differ from cellular respiration?",
        "Give me a one-sentence summary of both processes.",
    ]

    conversation_history = []

    with using_user(user_id), using_session(session_id):
        for turn_number, user_message in enumerate(turns, start=1):
            print(f"\n--- Turn {turn_number} ---")
            print(f"User: {user_message}")

            # Append the new user message to the running history
            conversation_history.append({"role": "user", "content": user_message})

            # Each call is auto-traced with the same user.id and session.id
            response = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=conversation_history,
            )

            assistant_message = response.choices[0].message.content
            conversation_history.append({"role": "assistant", "content": assistant_message})

            print(f"Assistant: {assistant_message}")


run_conversation(user_id="user-7f3a2b", session_id="session-c91d4e")

Expected output:

--- Turn 1 ---
User: What is photosynthesis?
Assistant: Photosynthesis is the process by which plants, algae, and some bacteria
convert sunlight, water, and carbon dioxide into glucose and oxygen...

--- Turn 2 ---
User: How does it differ from cellular respiration?
Assistant: While photosynthesis converts energy from light into stored chemical energy
(glucose), cellular respiration does the reverse — it breaks down glucose...

--- Turn 3 ---
User: Give me a one-sentence summary of both processes.
Assistant: Photosynthesis builds glucose from sunlight and CO₂, while cellular
respiration breaks glucose down to release energy for the cell.

In the Tracing dashboard, all three LLM spans share the same session.id and user.id attributes. To see them grouped as a conversation, click the Sessions tab (second tab after “LLM Tracing”) — you will see a session row with trace count, duration, and first/last messages. Click the session row to view all three turns together in a conversation view.

Note

The Sessions tab displays an auto-generated UUID as the session identifier — this is not the string you passed to using_session(). Your string (e.g., "session-c91d4e") is stored as the session name and used for grouping: all traces that share the same using_session() value within a project are linked to the same session.

Add per-turn metadata

Use using_metadata() to attach structured data to each individual turn: turn number, conversation stage, or any context that helps you analyze quality trends later. Nest it inside the outer using_user + using_session block so the turn-level metadata is scoped to that span only.

from fi_instrumentation import using_user, using_session, using_metadata

def run_conversation_with_metadata(user_id: str, session_id: str) -> None:
    """Same conversation loop with per-turn metadata attached to each span."""

    turns = [
        {"message": "What is photosynthesis?",                       "stage": "opening"},
        {"message": "How does it differ from cellular respiration?",  "stage": "deepening"},
        {"message": "Give me a one-sentence summary of both processes.", "stage": "closing"},
    ]

    conversation_history = []

    with using_user(user_id), using_session(session_id):
        for turn_number, turn in enumerate(turns, start=1):
            user_message = turn["message"]
            conversation_history.append({"role": "user", "content": user_message})

            # Per-turn metadata is scoped to this LLM span only
            turn_metadata = {
                "turn_number": turn_number,
                "conversation_stage": turn["stage"],
                "total_turns": len(turns),
            }

            with using_metadata(turn_metadata):
                response = client.chat.completions.create(
                    model="gpt-4o-mini",
                    messages=conversation_history,
                )

            assistant_message = response.choices[0].message.content
            conversation_history.append({"role": "assistant", "content": assistant_message})

            print(f"Turn {turn_number} [{turn['stage']}]: {assistant_message[:80]}...")


run_conversation_with_metadata(user_id="user-7f3a2b", session_id="session-c91d4e")

Expected output:

Turn 1 [opening]: Photosynthesis is the process by which plants, algae, and some b...
Turn 2 [deepening]: While photosynthesis converts light energy into stored chemical...
Turn 3 [closing]: Photosynthesis builds glucose from sunlight and CO₂, while cellu...

Each span in Tracing now carries a metadata attribute containing turnNumber, conversationStage, and totalTurns — visible in the span detail panel when you click any trace row. You can also filter by userId in the LLM Tracing tab to see all spans from a specific user across sessions.

Tip

You can combine using_user(), using_session(), using_metadata(), and using_tags() into a single using_attributes() call. Import it from fi_instrumentation.

View grouped sessions in the Tracing dashboard

The complete script below puts everything together. Run it once and then open the Tracing dashboard to inspect the full session.

import os
from fi_instrumentation import register, using_user, using_session, using_metadata
from fi_instrumentation.fi_types import ProjectType
from traceai_openai import OpenAIInstrumentor
from openai import OpenAI

# Setup tracing
trace_provider = register(
    project_type=ProjectType.OBSERVE,
    project_name="chatbot-session-demo",
)
OpenAIInstrumentor().instrument(tracer_provider=trace_provider)
client = OpenAI()

# Conversation data
USER_ID    = "user-7f3a2b"
SESSION_ID = "session-c91d4e"

turns = [
    {"message": "What is photosynthesis?",                            "stage": "opening"},
    {"message": "How does it differ from cellular respiration?",       "stage": "deepening"},
    {"message": "Give me a one-sentence summary of both processes.",   "stage": "closing"},
]

# Multi-turn loop
conversation_history = []

with using_user(USER_ID), using_session(SESSION_ID):
    for turn_number, turn in enumerate(turns, start=1):
        user_message = turn["message"]
        conversation_history.append({"role": "user", "content": user_message})

        with using_metadata({
            "turn_number": turn_number,
            "conversation_stage": turn["stage"],
            "total_turns": len(turns),
        }):
            response = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=conversation_history,
            )

        assistant_message = response.choices[0].message.content
        conversation_history.append({"role": "assistant", "content": assistant_message})

        print(f"Turn {turn_number} [{turn['stage']}]")
        print(f"  User:      {user_message}")
        print(f"  Assistant: {assistant_message[:100]}...")
        print()

print(f"Session complete. View at: app.futureagi.com → Tracing → Sessions tab")

trace_provider.force_flush()

Expected output:

Turn 1 [opening]
  User:      What is photosynthesis?
  Assistant: Photosynthesis is the process by which green plants, algae, and some bacteria...

Turn 2 [deepening]
  User:      How does it differ from cellular respiration?
  Assistant: Photosynthesis and cellular respiration are essentially opposite processes...

Turn 3 [closing]
  User:      Give me a one-sentence summary of both processes.
  Assistant: Photosynthesis converts light energy into glucose using CO₂ and water...

Session complete. View at: app.futureagi.com → Tracing → Sessions tab

Navigate to the Sessions tab to see the full conversation:

  1. Open app.futureagi.comTracing (left sidebar under OBSERVE) → select your project
  2. Click the Sessions tab (second tab after “LLM Tracing”)
  3. Your session appears as a row showing total traces (3), duration, first/last messages, and user ID
  4. Click the session row to open the conversation view — all three turns are displayed as a Human/AI conversation thread
  5. To find traces by user instead, switch to the LLM Tracing tab and filter by userId

Tip

Use a unique session_id per conversation and a stable user_id per user (e.g., their database UUID). The Sessions tab groups all traces sharing the same using_session() value, while the LLM Tracing tab lets you filter by userId to see all spans from a specific user across sessions.

What you built

You can now tag multi-turn conversations with user and session IDs, attach per-turn metadata, and view grouped sessions in the FutureAGI Tracing dashboard.

  • Registered a FutureAGI tracer provider and auto-instrumented OpenAI with OpenAIInstrumentor
  • Tagged every LLM span in a request with using_user() and using_session() so spans are linked to a specific user and conversation
  • Built a 3-turn chatbot loop where all spans share the same session.id and appear grouped in the Tracing dashboard
  • Attached per-turn metadata (turn_number, conversation_stage) to each span using using_metadata() — scoped to individual turns inside the shared session block
  • Viewed grouped sessions in the Sessions tab where each conversation appears as a unit, and filtered by userId in the LLM Tracing tab

Next steps

Was this page helpful?

Questions & Discussion