Automatic instrumentation

By default, Laminar.initialize() will automatically instrument majority of common LLM and VectorDB libraries for tracing. This includes OpenAI, Anthropic, Langchain, Pinecone, and many more.

Instrument all available libraries

from lmnr import Laminar as L
import os
from openai import OpenAI
# ...
L.initialize(project_api_key=os.environ["LMNR_PROJECT_API_KEY"])

See all available auto-instrumentable modules here.

Disable automatic instrumentation

initialize() also accepts an optional instruments parameter. If you explicitly pass an empty set, no automatic instrumentations will be applied.

from lmnr import Laminar as L
import os
from openai import OpenAI

L.initialize(
    project_api_key=os.environ["LMNR_PROJECT_API_KEY"],
    instruments=set()
)

# When you call OpenAI, it will NOT be instrumented

Instrument specific modules only

You can also enable instrumentation for specific modules only.

This is useful if you either want more control over what is being instrumented. Also, use this if you are using NextJS and having issues with automatic instrumentation – learn more.

Let’s say, for example, we call OpenAI and Anthropic models to perform the same task, and we only want to instrument the Anthropic calls, but not OpenAI.

initialize() accepts an optional instruments parameter. Pass a set of instruments you want to enable. In this case we only want to pass Instruments.ANTHROPIC.

See available instruments in the next subsection.

import os
from anthropic import Anthropic
from openai import OpenAI

from lmnr import observe, Laminar as L, Instruments

L.initialize(
    project_api_key=os.environ["LMNR_PROJECT_API_KEY"],
    instruments={ Instruments.ANTHROPIC }  # only enable anthropic, not openai
)

openai_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
anthropic_client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

def poem_writer(topic="turbulence"):
    prompt = f"write a poem about {topic}"
    messages = [
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": prompt},
    ]

    # OpenAI calls are NOT instrumented
    openai_response = openai_client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
    )

    # Anthropic calls are instrumented
    anthropic_response = anthropic_client.messages.create(
        max_tokens=1024,
        messages=messages,
        model="claude-3-5-sonnet-20240620",
    )

    openai_poem = openai_response.choices[0].message.content
    anthropic_poem = anthropic_response.content[0].text

    return {"o": openai_poem, "a": anthropic_poem}

if __name__ == "__main__":
    print(poem_writer(topic="laminar flow"))

Available instruments

See available instruments by importing Instruments from lmnr or view source.

These exact modules are auto-instrumented, if you do not pass instruments to L.initialize().

Manual instrumentation

Use observe to group separate LLM calls in one trace

Automatic instrumentation creates spans for LLM calls within the current trace context.

Unless you start a new trace before calling an LLM, each LLM call will create a new trace.

If you want to group several auto-instrumented calls in one trace, simply observe the top-level function that makes these calls.

Example

In this example, the request_handler makes a call to OpenAI to determine the user intent. If the intent matches the expected one, the handler makes another call to OpenAI (possibly with additional RAG) to generate a response.

request_handler is observed, so all calls to OpenAI inside it are grouped in one trace.

from lmnr import Laminar as L, observe
from openai import OpenAI
import os

L.initialize(project_api_key=os.environ['LMNR_PROJECT_API_KEY'])

openai_client = OpenAI(api_key=os.environ['OPENAI_API_KEY'])

@observe()
def request_handler(user_message: str):
    router_prompt = """Your goal is to determine if the user is asking for \
help with onboarding to the service. Answer 'yes' or 'no' without any \
explanation.""" + f"User message: {user_message}"

    user_intent = openai_client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "user",
                "content": router_prompt
            }
        ]
    )
    if user_intent.choices[0].message.content == "yes":
        # likely some RAG here to enrich the context
        # ...

        model_response = openai_client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {
                    "role": "user",
                    "content": user_message
                }
            ]
        )
        return model_response.choices[0].message.content
    return "the user is not asking for help with onboarding to the service"

As a result, you will get a nested trace with the request_handler span as the top level span, and the OpenAI calls as child spans.

Observe specific code chunks

Also, in Python, you can use start_as_current_span if you want to record a chunk of your code using with statement.

Example

from lmnr import Laminar as L
from openai import OpenAI

L.initialize(project_api_key=os.environ['LMNR_PROJECT_API_KEY'])
openai_client = OpenAI(api_key=os.environ['OPENAI_API_KEY'])

def request_handler(user_message: str):
    with L.start_as_current_span(name="handler", input=user_message) as span:
        response = openai_client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {
                    "role": "user",
                    "content": user_message
                }
            ]
        )
        result = response.choices[0].message.content
        
        L.set_span_output(result) # this will set the output of the current span

        return result