# Observations

Observations are the **building blocks** inside a trace. While a trace captures the complete end-to-end operation, observations record what happened at **each individual step**: model calls, tool invocations, retrieval operations, and any other discrete action in your workflow.

Observations can be nested, forming hierarchies that reflect the structure of your application's pipeline. Each observation contains a start time and its latency, a name and type, attributes (key-value metadata), and parent-child relationships for hierarchical traces.

You can navigate to `https://app.interactive.ai/project/<your_project_id>/observations` to see all observations in your project.&#x20;

{% hint style="info" %}
For the full Tracing API reference including all method signatures, parameters, and advanced options, see the [SDK Documentation](https://app.gitbook.com/s/jHEEbkpMbUW2x51XS8Ez/tracing).
{% endhint %}

### Why Observations Matter

Observations give you granular visibility into each step of your LLM pipeline. With observations, you can:

* **Measure latency and cost** at each step, not just end-to-end
* **Identify** which specific operation caused a failure or unexpected result
* **Compare performance** across different model versions or configurations
* Build datasets from specific observations for **targeted evaluation**

***

### Observation Types

InteractiveAI supports several observation types, each designed for specific kinds of operations:

<table><thead><tr><th width="212.8662109375">Type</th><th>Description</th></tr></thead><tbody><tr><td><img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2FxZ6E8BllJ7rZZeWYPPu4%2FType%3DSpan.svg?alt=media&#x26;token=7f91d476-cacf-422d-be98-b71aeac470d6" alt="" data-size="line">  <strong>Span</strong></td><td>A unit of work with a start and end time; use for any operation you want to measure</td></tr><tr><td><img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2Fg7d2DIiUH7WrlmDRoyCZ%2FType%3DGeneration.svg?alt=media&#x26;token=b53c3b68-f826-4e85-8323-6ff12489759a" alt="" data-size="line">  <strong>Generation</strong></td><td>An LLM call; captures prompts, completions, model details, token usage, and costs</td></tr><tr><td><img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2Fxp6GFuDtfKzLiBEj3FrZ%2FType%3DTool.svg?alt=media&#x26;token=f6248686-1dfd-45f4-b50e-ca357498fee0" alt="" data-size="line">  <strong>Tool</strong></td><td>A tool or function invocation; records the tool name, inputs, and outputs</td></tr><tr><td><img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2FrzI1HGPY226Q8JlxmjqS%2FType%3DEvent.svg?alt=media&#x26;token=e40a85e2-d1ae-49d6-a602-5505b9e5130e" alt="" data-size="line">  <strong>Event</strong></td><td>A discrete occurrence without duration; use for logging specific moments</td></tr><tr><td><img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2F8vRmLdev5mCFDKPWChEY%2FType%3DAgent.svg?alt=media&#x26;token=b1f6b3bb-e594-491f-a9be-8fb8f178cd34" alt="" data-size="line">. <strong>Agent</strong></td><td>An orchestration step where an LLM decides what action to take</td></tr><tr><td><img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2FehxXXL4d3nwHpUizsYIc%2FType%3DChain.svg?alt=media&#x26;token=ac61a65c-6237-4c4c-b791-65935cc7e172" alt="" data-size="line">  <strong>Chain</strong></td><td>A sequence of operations linked together</td></tr><tr><td><img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2Fx6myPrtWQJYSo1xmqcPD%2FType%3DRetriever.svg?alt=media&#x26;token=54abc927-85ab-4fce-b959-92f3e6a1616f" alt="" data-size="line">  <strong>Retriever</strong></td><td>A data retrieval operation (e.g., querying a vector database)</td></tr><tr><td><img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2FtyaLLc3KCeHhZNIIyShp%2FType%3DEmbedding.svg?alt=media&#x26;token=3982fca3-e650-45e0-a1f4-7f079dbf69b8" alt="" data-size="line">  <strong>Embedding</strong></td><td>A vector embedding generation; captures model, token usage, and costs</td></tr><tr><td><img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2F80yYBJScF50Z2J6i0KAH%2FType%3DEvaluator.svg?alt=media&#x26;token=314dec57-5824-43c0-ab0f-8d7624d48f10" alt="" data-size="line">  <strong>Evaluator</strong></td><td>An evaluation function execution</td></tr><tr><td><img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2Fa9f8K9z5Nbu55UfPHyTA%2FType%3DGuardrail.svg?alt=media&#x26;token=697e75d0-ed40-412d-b705-cfd086a53930" alt="" data-size="line">  <strong>Guardrail</strong></td><td>A safety check against malicious input or unsafe content</td></tr></tbody></table>

`generation` and `embedding` are special types that accept additional parameters: `model`, `model_parameters`, `usage_details`, `cost_details`, `completion_start_time`, and `prompt`. All other types share the same base parameters.

***

### Creating Observations

Observations are always created **within the context of a trace**. When nested inside an existing trace, they automatically become children of the current span.

{% hint style="info" %}
`@observe` examples on this page assume `from interactiveai import observe` has been imported.
{% endhint %}

{% tabs %}
{% tab title="Context Manager" %}

```python
with interactiveai.start_as_current_observation(as_type="span", name="rag-pipeline") as span:
    span.update(input={"query": "What is RAG?"})

    with interactiveai.start_as_current_observation(
        as_type="retriever",
        name="vector-search"
    ) as retriever:
        results = search_documents("What is RAG?")
        retriever.update(
            input={"query": "What is RAG?", "top_k": 5},
            output={"documents": results}
        )

    with interactiveai.start_as_current_observation(
        as_type="generation",
        name="answer-generation",
        model="gpt-4"
    ) as generation:
        generation.update(
            input={"prompt": "Based on these documents, what is RAG?"},
            output={"response": "RAG is a technique..."}
        )

    span.update(output={"status": "completed"})

interactiveai.flush()
```

For complex pipelines, observations can be nested multiple levels deep:

```python
with interactiveai.start_as_current_observation(as_type="span", name="document-processor") as outer_span:
    outer_span.update(input={"request": "Process document"})

    with interactiveai.start_as_current_observation(as_type="generation", name="summarize") as gen1:
        gen1.update(
            input={"prompt": "Summarize the document"},
            output={"summary": "Document summary here"}
        )

    with interactiveai.start_as_current_observation(as_type="span", name="post-processing") as mid_span:
        mid_span.update(input={"task": "Extract key points from summary"})

        with interactiveai.start_as_current_observation(as_type="generation", name="extract") as gen2:
            gen2.update(
                input={"prompt": "Extract key points"},
                output={"key_points": ["Point 1", "Point 2"]}
            )

        mid_span.update(output={"status": "done"})

    outer_span.update(output={"status": "finished"})

interactiveai.flush()
```

{% endtab %}

{% tab title="@observe Decorator" %}
When decorated functions call other decorated functions, nesting happens automatically:

```python
@observe()
def rag_pipeline(query):
    results = vector_search(query)
    answer = generate_answer(query, results)
    return {"results": results, "answer": answer}

@observe(as_type="retriever", name="vector-search")
def vector_search(query):
    # Automatically becomes a child of rag_pipeline
    return search_documents(query)

@observe(as_type="generation", name="answer-generation")
def generate_answer(query, context):
    # Automatically becomes a child of rag_pipeline
    return llm.generate(query=query, context=context)

# Creates: rag_pipeline → vector_search + answer_generation
rag_pipeline("What is RAG?")
interactiveai.flush()
```

This produces the same nested hierarchy as the context manager version, without manual nesting.
{% endtab %}
{% endtabs %}

***

### Updating Observations

After creating an observation, you can update it with additional data that becomes available during execution.

{% tabs %}
{% tab title="Context Manager" %}
Use `update()` on the observation object:

```python
with interactiveai.start_as_current_observation(as_type="span", name="process") as span:
    result = step_one()
    span.update(metadata={"step": "one", "intermediate_result": result})

    final = step_two(result)
    span.update(output={"final": final})
```

For generation and embedding observations, `update()` also accepts model-specific parameters:

```python
with interactiveai.start_as_current_observation(
    as_type="generation",
    name="llm-call",
    model="gpt-4"
) as generation:
    response = llm.generate(query)

    generation.update(
        output=response.text,
        usage_details={
            "prompt_tokens": response.usage.prompt_tokens,
            "completion_tokens": response.usage.completion_tokens
        },
        cost_details={"input": 0.0012, "output": 0.0035}
    )
```

{% endtab %}

{% tab title="@observe Decorator" %}
Inside a decorated function, use `update_current_span()` to add data to the current observation:

```python
@observe()
def process_pipeline(query):
    result = step_one(query)
    interactiveai.update_current_span(
        metadata={"step": "one", "intermediate_result": result}
    )

    final = step_two(result)
    interactiveai.update_current_span(output={"final": final})
    return final
```

For generation and embedding observations, use `update_current_generation()` which accepts model-specific parameters:

```python
@observe(as_type="generation", name="llm-call")
def generate_response(query):
    response = llm.generate(query)

    interactiveai.update_current_generation(
        model="gpt-4",
        model_parameters={"temperature": 0.7, "max_tokens": 500},
        output=response.text,
        usage_details={
            "prompt_tokens": response.usage.prompt_tokens,
            "completion_tokens": response.usage.completion_tokens
        },
        cost_details={"input": 0.0012, "output": 0.0035}
    )
    return response.text
```

{% endtab %}
{% endtabs %}

***

### Properties of an Observation

| Property                | Description                                                                   |
| ----------------------- | ----------------------------------------------------------------------------- |
| **Name**                | The name of the observation (e.g., "llm-generation", "vector-search")         |
| **Start Time**          | Timestamp when the observation began                                          |
| **End Time**            | Timestamp when the observation completed                                      |
| **Input**               | JSON payload capturing the request or input data                              |
| **Output**              | JSON payload capturing the response or output data                            |
| **Level**               | Importance level to control verbosity: `DEBUG`, `DEFAULT`, `WARNING`, `ERROR` |
| **Status Message**      | Additional information, such as error details when level is `ERROR`           |
| **Latency**             | Duration of the observation from start to end                                 |
| **Model**               | The model used (for generations and embeddings)                               |
| **Model Cost**          | Cost of the operation (for generations and embeddings)                        |
| **Time to First Token** | Time elapsed before the first token was received                              |
| **Tokens**              | Token count (input + output) for the observation                              |
| **Prompt**              | Link to prompt version in InteractiveAI prompt management                     |
| **Environment**         | Deployment context like `production`, `staging`, or `development`             |
| **Trace Tags**          | Tags inherited from the parent trace                                          |
| **Metadata**            | Free-form JSON for extra context specific to this observation                 |
| **Observation ID**      | Unique identifier (16-character lowercase hexadecimal string)                 |
| **Trace Name**          | Name of the parent trace containing this observation                          |
| **Trace ID**            | Unique identifier of the parent trace                                         |

***

### Observation Type Examples

#### <img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2FrPukJi1uDMbpgVbafhwJ%2FType%3DSpan%2010.38.45.svg?alt=media&#x26;token=40401c2b-3740-4669-a03a-f8abc3fc998f" alt="" data-size="line"> Span

A generic unit of work. Use when no specialized type fits your operation:

{% tabs %}
{% tab title="Context Manager" %}

```python
with interactiveai.start_as_current_observation(
    as_type="span",
    name="data-preprocessing"
) as span:
    cleaned = clean_data(raw_input)
    validated = validate_schema(cleaned)
    span.update(
        input={"raw_records": len(raw_input)},
        output={"valid_records": len(validated), "rejected": len(raw_input) - len(validated)}
    )
```

{% endtab %}

{% tab title="@observe Decorator" %}

```python
@observe(as_type="span", name="data-preprocessing")
def preprocess(raw_input):
    cleaned = clean_data(raw_input)
    validated = validate_schema(cleaned)
    return {"valid_records": len(validated), "rejected": len(raw_input) - len(validated)}

preprocess(raw_input)
```

{% endtab %}
{% endtabs %}

Agent, Chain, and Evaluator work identically to Span but are visually differentiated in the InteractiveAI UI for filtering and organization. Use `as_type="agent"`, `as_type="chain"`, or `as_type="evaluator"` respectively.

#### <img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2FkO3vT6PBQbKXR9agxr8C%2FType%3DGeneration.svg?alt=media&#x26;token=e8bb37cb-26db-4656-8a1b-1f48138bbfd9" alt="" data-size="line"> Generation

Captures model, token usage, and costs:

{% tabs %}
{% tab title="Context Manager" %}

```python
with interactiveai.start_as_current_observation(
    as_type="generation",
    name="chat-completion",
    model="gpt-4",
    model_parameters={"temperature": 0.7, "max_tokens": 500}
) as generation:
    generation.update(
        input={"messages": [{"role": "user", "content": "Explain quantum computing"}]},
        output={"response": "Quantum computing uses quantum mechanical phenomena..."},
        usage_details={"prompt_tokens": 12, "completion_tokens": 85},
        cost_details={"input": 0.0004, "output": 0.0051}
    )
```

{% endtab %}

{% tab title="@observe Decorator" %}

```python
@observe(as_type="generation", name="chat-completion")
def chat_completion(messages):
    response = openai_client.chat.completions.create(
        model="gpt-4",
        messages=messages,
        temperature=0.7,
        max_tokens=500
    )

    interactiveai.update_current_generation(
        model="gpt-4",
        model_parameters={"temperature": 0.7, "max_tokens": 500},
        usage_details={
            "prompt_tokens": response.usage.prompt_tokens,
            "completion_tokens": response.usage.completion_tokens
        }
    )
    return response.choices[0].message.content

chat_completion([{"role": "user", "content": "Explain quantum computing"}])
interactiveai.flush()
```

{% endtab %}
{% endtabs %}

#### <img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2FtkpyxiFzhfW0qqKRbscy%2FType%3DTool.svg?alt=media&#x26;token=c3bfcc42-2413-469f-a7b5-0e945eea5ced" alt="" data-size="line"> Tool

Use for function or tool invocations:

{% tabs %}
{% tab title="Context Manager" %}

```python
with interactiveai.start_as_current_observation(
    as_type="tool",
    name="web-search"
) as tool:
    tool.update(
        input={"query": "latest AI news"},
        output={"results": [{"title": "Article 1", "url": "https://..."}]}
    )
```

{% endtab %}

{% tab title="@observe Decorator" %}

```python
@observe(as_type="tool", name="web-search")
def web_search(query):
    results = search_engine.search(query)
    return {"results": results}

web_search("latest AI news")
```

{% endtab %}
{% endtabs %}

#### <img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2Fhcliwyz9xLfrhgaDmQH1%2FType%3DRetriever.svg?alt=media&#x26;token=312d20b4-ff1f-42d0-b9e5-0eb552842ed8" alt="" data-size="line"> Retriever

Use for vector database queries or document retrieval:

{% tabs %}
{% tab title="Context Manager" %}

```python
with interactiveai.start_as_current_observation(
    as_type="retriever",
    name="vector-search"
) as retriever:
    retriever.update(
        input={"query": "machine learning basics", "top_k": 5},
        output={"documents": [{"id": "doc1", "score": 0.95, "content": "..."}]}
    )
```

{% endtab %}

{% tab title="@observe Decorator" %}

```python
@observe(as_type="retriever", name="vector-search")
def vector_search(query, top_k=5):
    results = vector_db.search(query, top_k=top_k)
    return {"documents": results}

vector_search("machine learning basics", top_k=5)
```

{% endtab %}
{% endtabs %}

#### <img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2FgpsuAf5QAuSkGaDX2eI4%2FType%3DEmbedding.svg?alt=media&#x26;token=4761074b-8009-4a15-9e93-bc1eb346ded6" alt="" data-size="line"> Embedding

Use for embedding generation. Like `generation`, this type captures model, token usage, and costs:

{% tabs %}
{% tab title="Context Manager" %}

```python
with interactiveai.start_as_current_observation(
    as_type="embedding",
    name="document-embedding",
    model="text-embedding-3-small"
) as embedding:
    embedding.update(
        input={"text": "Retrieval-Augmented Generation combines retrieval with language model generation..."},
        output={"vector": [0.023, -0.041, 0.117, ...]},
        usage_details={"prompt_tokens": 14}
    )
```

{% endtab %}

{% tab title="@observe Decorator" %}

```python
@observe(as_type="embedding", name="document-embedding")
def embed_document(text):
    response = openai_client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    # Embedding is a generation-like type internally, so use update_current_generation
    interactiveai.update_current_generation(
        model="text-embedding-3-small",
        usage_details={"prompt_tokens": response.usage.prompt_tokens}
    )
    return response.data[0].embedding

embed_document("Retrieval-Augmented Generation combines retrieval with language model generation...")
```

{% endtab %}
{% endtabs %}

#### <img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2FKkWOFZaxeznxbUEDsXfX%2FType%3DEvent.svg?alt=media&#x26;token=1e18251e-2dcb-40fb-8bb6-fee09e2aac3f" alt="" data-size="line"> Event

A discrete, zero-duration occurrence. Use for logging specific moments like user feedback, system events, or state changes. Events use the dedicated `create_event()` method — they cannot be created with `@observe` or `start_as_current_observation` because they have no duration to wrap around.

```python
interactiveai.create_event(
    name="user-feedback-received",
    input={"feedback_type": "thumbs_up", "message_id": "msg-789"},
    metadata={"source": "web-app"}
)
```

`create_event()` accepts the same base parameters as other observation types (`name`, `input`, `output`, `metadata`, `version`, `level`, `status_message`) but creates an observation that starts and ends at the same instant.

#### <img src="https://708770081-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1ICwJbq7EJdn5kBgXnQu%2Fuploads%2FsF2c1QctTeyogfvk2o0A%2FType%3DGuardrail.svg?alt=media&#x26;token=2ac9ac08-cb27-4a09-9e77-f6039340c162" alt="" data-size="line"> Guardrail

Use for safety checks:

{% tabs %}
{% tab title="Context Manager"" %}

```python
with interactiveai.start_as_current_observation(
    as_type="guardrail",
    name="content-safety-check"
) as guardrail:
    guardrail.update(
        input={"content": "User message to check"},
        output={"safe": True, "categories": {"toxicity": False, "hate": False}}
    )
```

{% endtab %}

{% tab title="@observe Decorator" %}

```python
@observe(as_type="guardrail", name="content-safety-check")
def check_content_safety(content):
    result = safety_classifier.check(content)
    return {"safe": result.is_safe, "categories": result.categories}

check_content_safety("User message to check")
```

{% endtab %}
{% endtabs %}
