Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from __future__ import annotations

from typing import Any, Optional
from typing import Any, Optional, cast
from uuid import UUID

from langchain_core.callbacks import BaseCallbackHandler
Expand All @@ -29,6 +29,7 @@
Error,
InputMessage,
LLMInvocation,
MessagePart,
OutputMessage,
Text,
)
Expand Down Expand Up @@ -133,7 +134,11 @@ def on_chat_model_start(
Text(content=text_value, type="text")
)

input_messages.append(InputMessage(parts=parts, role=role))
input_messages.append(
InputMessage(
parts=cast(list[MessagePart], parts), role=role
)
)

llm_invocation = LLMInvocation(
request_model=request_model,
Expand Down Expand Up @@ -206,7 +211,7 @@ def on_llm_end(
role = chat_generation.message.type
output_message = OutputMessage(
role=role,
parts=parts,
parts=cast(list[MessagePart], parts),
finish_reason=finish_reason,
)
output_messages.append(output_message)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from __future__ import annotations

from unittest import mock
from uuid import uuid4

from opentelemetry.instrumentation.langchain.callback_handler import (
GEN_AI_MEMORY_QUERY,
GEN_AI_MEMORY_SEARCH_RESULT_COUNT,
GEN_AI_MEMORY_STORE_ID,
GEN_AI_MEMORY_STORE_NAME,
RETRIEVAL_OPERATION,
SEARCH_MEMORY_OPERATION,
OpenTelemetryLangChainCallbackHandler,
)
from opentelemetry.util.genai.types import ContentCapturingMode


def _build_handler():
telemetry_handler = mock.Mock()

def _start(invocation):
span = mock.Mock()
span.is_recording.return_value = True
invocation.span = span
return invocation

telemetry_handler.start_llm.side_effect = _start
telemetry_handler.stop_llm.side_effect = lambda invocation: invocation
telemetry_handler.fail_llm.side_effect = (
lambda invocation, error: invocation
)
return (
OpenTelemetryLangChainCallbackHandler(telemetry_handler),
telemetry_handler,
)


def test_retriever_defaults_to_retrieval_without_memory_metadata(monkeypatch):
"""Retrievers without memory metadata should emit 'retrieval' operation."""
handler, telemetry_handler = _build_handler()
monkeypatch.setattr(
"opentelemetry.instrumentation.langchain.callback_handler.is_experimental_mode",
lambda: False,
)

run_id = uuid4()
handler.on_retriever_start(
serialized={"name": "PineconeRetriever"},
query="what is RAG?",
run_id=run_id,
metadata={"ls_provider": "pinecone"},
)

invocation = handler._invocation_manager.get_invocation(run_id)
assert invocation is not None
assert invocation.operation_name == RETRIEVAL_OPERATION
telemetry_handler.start_llm.assert_called_once()


def test_retriever_uses_search_memory_with_memory_metadata(monkeypatch):
"""Retrievers with memory_store_name in metadata should emit 'search_memory'."""
handler, telemetry_handler = _build_handler()
monkeypatch.setattr(
"opentelemetry.instrumentation.langchain.callback_handler.is_experimental_mode",
lambda: True,
)
monkeypatch.setattr(
"opentelemetry.instrumentation.langchain.callback_handler.get_content_capturing_mode",
lambda: ContentCapturingMode.SPAN_ONLY,
)

run_id = uuid4()
handler.on_retriever_start(
serialized={
"name": "SessionMemoryRetriever",
"id": ["langchain", "retriever", "session"],
},
query="user preferences",
run_id=run_id,
metadata={
"ls_provider": "openai",
"memory_store_name": "SessionMemoryRetriever",
"memory_namespace": "user-123",
},
)

invocation = handler._invocation_manager.get_invocation(run_id)
assert invocation is not None
assert invocation.operation_name == SEARCH_MEMORY_OPERATION
assert (
invocation.attributes[GEN_AI_MEMORY_STORE_NAME]
== "SessionMemoryRetriever"
)
assert (
invocation.attributes[GEN_AI_MEMORY_STORE_ID]
== "langchain.retriever.session"
)
assert invocation.attributes[GEN_AI_MEMORY_QUERY] == "user preferences"
telemetry_handler.start_llm.assert_called_once()


def test_on_retriever_end_sets_search_result_count(monkeypatch):
handler, telemetry_handler = _build_handler()
monkeypatch.setattr(
"opentelemetry.instrumentation.langchain.callback_handler.is_experimental_mode",
lambda: False,
)

run_id = uuid4()
handler.on_retriever_start(
serialized={"name": "MemoryRetriever"},
query="q",
run_id=run_id,
metadata={"ls_provider": "openai"},
)
handler.on_retriever_end(documents=[object(), object()], run_id=run_id)

telemetry_handler.stop_llm.assert_called_once()
stop_invocation = telemetry_handler.stop_llm.call_args.kwargs["invocation"]
assert stop_invocation.attributes[GEN_AI_MEMORY_SEARCH_RESULT_COUNT] == 2


def test_on_retriever_error_fails_invocation(monkeypatch):
handler, telemetry_handler = _build_handler()
monkeypatch.setattr(
"opentelemetry.instrumentation.langchain.callback_handler.is_experimental_mode",
lambda: False,
)

run_id = uuid4()
handler.on_retriever_start(
serialized={"name": "VectorRetriever"},
query="q",
run_id=run_id,
metadata={"ls_provider": "openai"},
)
handler.on_retriever_error(RuntimeError("retrieval failed"), run_id=run_id)

telemetry_handler.fail_llm.assert_called_once()
fail_invocation = telemetry_handler.fail_llm.call_args.kwargs["invocation"]
assert fail_invocation.operation_name == RETRIEVAL_OPERATION
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

- Initial release: Mem0 memory operation instrumentation aligned with GenAI
memory semantic conventions.
([#3250](https://github.com/open-telemetry/semantic-conventions/pull/3250))
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

See https://www.apache.org/licenses/LICENSE-2.0 for the full license text.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
OpenTelemetry Mem0 Instrumentation
===================================

|pypi|

.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-mem0.svg
:target: https://pypi.org/project/opentelemetry-instrumentation-mem0/

This library allows tracing Mem0 memory operations (add, search, update,
delete) using OpenTelemetry, emitting spans with GenAI memory semantic
convention attributes.

Installation
------------

::

pip install opentelemetry-instrumentation-mem0

Usage
-----

.. code-block:: python

from opentelemetry.instrumentation.mem0 import Mem0Instrumentor
from mem0 import Memory

Mem0Instrumentor().instrument()

m = Memory()
m.add("I prefer dark mode", user_id="alice")
results = m.search("preferences", user_id="alice")

References
----------

* `OpenTelemetry Project <https://opentelemetry.io/>`_
* `Mem0 <https://github.com/mem0ai/mem0>`_
* `GenAI Memory Semantic Conventions <https://github.com/open-telemetry/semantic-conventions/pull/3250>`_
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "opentelemetry-instrumentation-mem0"
dynamic = ["version"]
description = "OpenTelemetry Mem0 instrumentation"
readme = "README.rst"
license = "Apache-2.0"
requires-python = ">=3.9"
authors = [
{ name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" },
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dependencies = [
"opentelemetry-api ~= 1.37",
"opentelemetry-instrumentation ~= 0.58b0",
"opentelemetry-semantic-conventions ~= 0.58b0",
"opentelemetry-util-genai ~= 0.58b0",
]

[project.optional-dependencies]
instruments = [
"mem0ai >= 0.1.0",
]

[project.entry-points.opentelemetry_instrumentor]
mem0 = "opentelemetry.instrumentation.mem0:Mem0Instrumentor"

[project.urls]
Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation-genai/opentelemetry-instrumentation-mem0"
Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib"

[tool.hatch.version]
path = "src/opentelemetry/instrumentation/mem0/version.py"

[tool.hatch.build.targets.sdist]
include = [
"/src",
"/tests",
"/examples",
]

[tool.hatch.build.targets.wheel]
packages = ["src/opentelemetry"]

[tool.pytest.ini_options]
testpaths = ["tests"]
Loading
Loading