Automatic instrumentation

By default, Laminar.initialize() will automatically instrument majority of the most popular LLM and VectorDB libraries for tracing. For example, 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.

No 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, or if you are using NextJS, where you need to manually pass all libraries which you want Laminar to auto-instrument.

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 context.

If, however, by the time of an LLM call, there was no active context, every call will end up in a separate trace.

If you want to group several LLM/VectorDB 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 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[:3].lower() == "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.

screenshot of the example span

Observe specific code chunks

Also, 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