From bd804fa805e1e590f95300f01ad1edf49a05eb16 Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Thu, 29 May 2025 19:17:23 +0530 Subject: [PATCH 01/23] Implement concurrent.futures instrumentation for OpenTelemetry context propagation --- agentops/instrumentation/__init__.py | 37 +- .../concurrent_futures/__init__.py | 10 + .../concurrent_futures/instrumentation.py | 159 ++++++ .../sdk/test_concurrent_instrumentation.py | 475 ++++++++++++++++++ 4 files changed, 678 insertions(+), 3 deletions(-) create mode 100644 agentops/instrumentation/concurrent_futures/__init__.py create mode 100644 agentops/instrumentation/concurrent_futures/instrumentation.py create mode 100644 tests/unit/sdk/test_concurrent_instrumentation.py diff --git a/agentops/instrumentation/__init__.py b/agentops/instrumentation/__init__.py index d4e271f3d..121754933 100644 --- a/agentops/instrumentation/__init__.py +++ b/agentops/instrumentation/__init__.py @@ -66,9 +66,10 @@ def _uninstrument_providers(): def _should_instrument_package(package_name: str) -> bool: """ Determine if a package should be instrumented based on current state. - Handles special cases for agentic libraries and providers. + Handles special cases for agentic libraries, providers, and utility instrumentors. """ global _has_agentic_library + # If this is an agentic library, uninstrument all providers first if package_name in AGENTIC_LIBRARIES: _uninstrument_providers() @@ -78,6 +79,10 @@ def _should_instrument_package(package_name: str) -> bool: # Skip providers if an agentic library is already instrumented if package_name in PROVIDERS and _has_agentic_library: return False + + # Utility instrumentors are always enabled regardless of agentic library state + if package_name in UTILITY_INSTRUMENTORS: + return not _is_package_instrumented(package_name) # Skip if already instrumented if _is_package_instrumented(package_name): @@ -93,7 +98,10 @@ def _perform_instrumentation(package_name: str): return # Get the appropriate configuration for the package - config = PROVIDERS.get(package_name) or AGENTIC_LIBRARIES[package_name] + config = PROVIDERS.get(package_name) or AGENTIC_LIBRARIES.get(package_name) or UTILITY_INSTRUMENTORS.get(package_name) + if not config: + return + loader = InstrumentorLoader(**config) if loader.should_activate: @@ -143,6 +151,7 @@ def _import_monitor(name: str, globals_dict=None, locals_dict=None, fromlist=(), # Instrument all matching packages for package_to_check in packages_to_check: if package_to_check not in _instrumenting_packages and not _is_package_instrumented(package_to_check): + _instrumenting_packages.add(package_to_check) try: _perform_instrumentation(package_to_check) @@ -188,6 +197,22 @@ class InstrumentorConfig(TypedDict): "min_version": "0.1.0", "package_name": "google-genai", # Actual pip package name }, + # "mem0": { + # "module_name": "agentops.instrumentation.mem0", + # "class_name": "Mem0Instrumentor", + # "min_version": "0.1.10", + # "package_name": "mem0ai", # Actual pip package name + # }, +} + +# Configuration for utility instrumentors +UTILITY_INSTRUMENTORS: dict[str, InstrumentorConfig] = { + "concurrent.futures": { + "module_name": "agentops.instrumentation.concurrent_futures", + "class_name": "ConcurrentFuturesInstrumentor", + "min_version": "3.7.0", # Python 3.7+ (concurrent.futures is stdlib) + "package_name": "python", # Special case for stdlib modules + }, } # Configuration for supported agentic libraries @@ -211,7 +236,7 @@ class InstrumentorConfig(TypedDict): } # Combine all target packages for monitoring -TARGET_PACKAGES = set(PROVIDERS.keys()) | set(AGENTIC_LIBRARIES.keys()) +TARGET_PACKAGES = set(PROVIDERS.keys()) | set(AGENTIC_LIBRARIES.keys()) | set(UTILITY_INSTRUMENTORS.keys()) # Create a single instance of the manager # _manager = InstrumentationManager() # Removed @@ -238,6 +263,12 @@ def module(self) -> ModuleType: def should_activate(self) -> bool: """Check if the package is available and meets version requirements.""" try: + # Special case for stdlib modules (like concurrent.futures) + if self.package_name == "python": + import sys + python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + return Version(python_version) >= parse(self.min_version) + # Use explicit package_name if provided, otherwise derive from module_name if self.package_name: provider_name = self.package_name diff --git a/agentops/instrumentation/concurrent_futures/__init__.py b/agentops/instrumentation/concurrent_futures/__init__.py new file mode 100644 index 000000000..716b95f3f --- /dev/null +++ b/agentops/instrumentation/concurrent_futures/__init__.py @@ -0,0 +1,10 @@ +""" +Instrumentation for concurrent.futures module. + +This module provides automatic instrumentation for ThreadPoolExecutor to ensure +proper OpenTelemetry context propagation across thread boundaries. +""" + +from .instrumentation import ConcurrentFuturesInstrumentor + +__all__ = ["ConcurrentFuturesInstrumentor"] \ No newline at end of file diff --git a/agentops/instrumentation/concurrent_futures/instrumentation.py b/agentops/instrumentation/concurrent_futures/instrumentation.py new file mode 100644 index 000000000..bbd468e31 --- /dev/null +++ b/agentops/instrumentation/concurrent_futures/instrumentation.py @@ -0,0 +1,159 @@ +""" +OpenTelemetry Instrumentation for concurrent.futures module. + +This instrumentation automatically patches ThreadPoolExecutor to ensure proper +context propagation across thread boundaries, preventing "NEW TRACE DETECTED" issues. +""" + +import contextvars +import functools +import sys +from typing import Collection, Optional, Any, Callable +from concurrent.futures import ThreadPoolExecutor + +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.util._importlib_metadata import version + +from agentops.logging import logger + +# Store original methods to restore during uninstrumentation +_original_init = None +_original_submit = None + + +def _context_propagating_init(original_init): + """Wrap ThreadPoolExecutor.__init__ to set up context-aware initializer.""" + + @functools.wraps(original_init) + def wrapped_init(self, max_workers=None, thread_name_prefix='', initializer=None, initargs=()): + # Capture the current context when the executor is created + main_context = contextvars.copy_context() + + def context_aware_initializer(): + """Initializer that sets up the captured context in each worker thread.""" + logger.debug("[ConcurrentFuturesInstrumentor] Setting up context in worker thread") + + # Set the main context variables in this thread + for var, value in main_context.items(): + try: + var.set(value) + except Exception as e: + logger.debug(f"[ConcurrentFuturesInstrumentor] Could not set context var {var}: {e}") + + # Run user's initializer if provided + if initializer and callable(initializer): + try: + if initargs: + initializer(*initargs) + else: + initializer() + except Exception as e: + logger.error(f"[ConcurrentFuturesInstrumentor] Error in user initializer: {e}") + raise + + logger.debug("[ConcurrentFuturesInstrumentor] Worker thread context setup complete") + + # Create executor with context-aware initializer + prefix = f'AgentOps-{thread_name_prefix}' if thread_name_prefix else 'AgentOps-Thread' + + # Call original init with our context-aware initializer + original_init( + self, + max_workers=max_workers, + thread_name_prefix=prefix, + initializer=context_aware_initializer, + initargs=() # We handle initargs in our wrapper + ) + + logger.debug(f"[ConcurrentFuturesInstrumentor] ThreadPoolExecutor initialized with context propagation") + + return wrapped_init + + +def _context_propagating_submit(original_submit): + """Wrap ThreadPoolExecutor.submit to ensure context propagation.""" + + @functools.wraps(original_submit) + def wrapped_submit(self, func, *args, **kwargs): + # Log the submission + func_name = getattr(func, '__name__', str(func)) + logger.debug(f"[ConcurrentFuturesInstrumentor] Submitting function: {func_name}") + + # The context propagation is handled by the initializer, so we can submit normally + # But we can add additional logging or monitoring here if needed + return original_submit(self, func, *args, **kwargs) + + return wrapped_submit + + +class ConcurrentFuturesInstrumentor(BaseInstrumentor): + """ + Instrumentor for concurrent.futures module. + + This instrumentor patches ThreadPoolExecutor to automatically propagate + OpenTelemetry context to worker threads, ensuring all LLM calls and other + instrumented operations maintain proper trace context. + """ + + def instrumentation_dependencies(self) -> Collection[str]: + """Return a list of instrumentation dependencies.""" + return [] + + def _instrument(self, **kwargs): + """Instrument the concurrent.futures module.""" + global _original_init, _original_submit + + logger.debug("[ConcurrentFuturesInstrumentor] Starting instrumentation") + + # Store original methods + _original_init = ThreadPoolExecutor.__init__ + _original_submit = ThreadPoolExecutor.submit + + # Patch ThreadPoolExecutor methods + ThreadPoolExecutor.__init__ = _context_propagating_init(_original_init) + ThreadPoolExecutor.submit = _context_propagating_submit(_original_submit) + + logger.info("[ConcurrentFuturesInstrumentor] Successfully instrumented concurrent.futures.ThreadPoolExecutor") + + def _uninstrument(self, **kwargs): + """Uninstrument the concurrent.futures module.""" + global _original_init, _original_submit + + logger.debug("[ConcurrentFuturesInstrumentor] Starting uninstrumentation") + + # Restore original methods + if _original_init: + ThreadPoolExecutor.__init__ = _original_init + _original_init = None + + if _original_submit: + ThreadPoolExecutor.submit = _original_submit + _original_submit = None + + logger.info("[ConcurrentFuturesInstrumentor] Successfully uninstrumented concurrent.futures.ThreadPoolExecutor") + + @staticmethod + def instrument_module_directly(): + """ + Directly instrument the module without using the standard instrumentor interface. + + This can be called manually if automatic instrumentation is not desired. + """ + instrumentor = ConcurrentFuturesInstrumentor() + if not instrumentor.is_instrumented_by_opentelemetry: + instrumentor.instrument() + return True + return False + + @staticmethod + def uninstrument_module_directly(): + """ + Directly uninstrument the module. + + This can be called manually to remove instrumentation. + """ + instrumentor = ConcurrentFuturesInstrumentor() + if instrumentor.is_instrumented_by_opentelemetry: + instrumentor.uninstrument() + return True + return False \ No newline at end of file diff --git a/tests/unit/sdk/test_concurrent_instrumentation.py b/tests/unit/sdk/test_concurrent_instrumentation.py new file mode 100644 index 000000000..0dd2ac93b --- /dev/null +++ b/tests/unit/sdk/test_concurrent_instrumentation.py @@ -0,0 +1,475 @@ +""" +Unit tests for concurrent instrumentation and context propagation. + +This module tests the behavior of OpenTelemetry spans when using concurrent.futures.ThreadPoolExecutor, +specifically testing context propagation across thread boundaries. +""" + +import concurrent.futures +import contextvars +import time +import unittest +from unittest.mock import patch, MagicMock +import threading + +from opentelemetry import context, trace +from opentelemetry.sdk.trace import ReadableSpan, TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + +from agentops.sdk.processors import InternalSpanProcessor + + +class IsolatedInstrumentationTester: + """ + A lighter-weight instrumentation tester that doesn't affect global state. + + This version creates an isolated tracer provider and doesn't shut down + the global tracing core, making it safer for use alongside other tests. + """ + + def __init__(self): + """Initialize with isolated tracer provider.""" + # Create isolated tracer provider and exporter + self.tracer_provider = TracerProvider() + self.memory_exporter = InMemorySpanExporter() + self.span_processor = SimpleSpanProcessor(self.memory_exporter) + self.tracer_provider.add_span_processor(self.span_processor) + + # Don't set as global provider - keep isolated + self.tracer = self.tracer_provider.get_tracer(__name__) + + def get_tracer(self): + """Get the isolated tracer.""" + return self.tracer + + def clear_spans(self): + """Clear all spans from the memory exporter.""" + self.span_processor.force_flush() + self.memory_exporter.clear() + + def get_finished_spans(self): + """Get all finished spans.""" + self.span_processor.force_flush() + return list(self.memory_exporter.get_finished_spans()) + + +class TestConcurrentInstrumentation(unittest.TestCase): + """Tests for concurrent instrumentation and context propagation.""" + + def setUp(self): + """Set up test environment with isolated instrumentation tester.""" + self.tester = IsolatedInstrumentationTester() + self.tracer = self.tester.get_tracer() + + def tearDown(self): + """Clean up test environment without affecting global state.""" + # Only clear our isolated spans + self.tester.clear_spans() + + def _create_simple_span(self, name: str, sleep_duration: float = 0.01) -> str: + """Helper to create a simple span and return its trace_id.""" + with self.tracer.start_as_current_span(name) as span: + time.sleep(sleep_duration) # Simulate work + return span.get_span_context().trace_id + + def _create_nested_spans(self, parent_name: str, child_name: str) -> tuple: + """Helper to create nested spans and return their trace_ids.""" + with self.tracer.start_as_current_span(parent_name) as parent_span: + parent_trace_id = parent_span.get_span_context().trace_id + time.sleep(0.01) + + with self.tracer.start_as_current_span(child_name) as child_span: + child_trace_id = child_span.get_span_context().trace_id + time.sleep(0.01) + + return parent_trace_id, child_trace_id + + def test_sequential_spans_same_trace(self): + """Test that sequential spans in the same thread share the same trace.""" + trace_id1 = self._create_simple_span("span1") + trace_id2 = self._create_simple_span("span2") + + # In sequential execution, spans should be independent (different traces) + spans = self.tester.get_finished_spans() + self.assertEqual(len(spans), 2) + + # Each span should be a root span (no parent) + for span in spans: + self.assertIsNone(span.parent) + + def test_nested_spans_same_trace(self): + """Test that nested spans share the same trace.""" + parent_trace_id, child_trace_id = self._create_nested_spans("parent", "child") + + # Nested spans should share the same trace + self.assertEqual(parent_trace_id, child_trace_id) + + spans = self.tester.get_finished_spans() + self.assertEqual(len(spans), 2) + + # Find parent and child spans + parent_spans = [s for s in spans if s.name == "parent"] + child_spans = [s for s in spans if s.name == "child"] + + self.assertEqual(len(parent_spans), 1) + self.assertEqual(len(child_spans), 1) + + parent_span = parent_spans[0] + child_span = child_spans[0] + + # Child should have parent as its parent + self.assertEqual(child_span.parent.span_id, parent_span.context.span_id) + + def test_threadpool_without_context_propagation_creates_separate_traces(self): + """Test that ThreadPoolExecutor without context propagation creates separate traces.""" + def worker_task(task_id: str) -> dict: + """Worker task that creates a span without context propagation.""" + with self.tracer.start_as_current_span(f"worker_task_{task_id}") as span: + time.sleep(0.01) # Simulate work + return { + "task_id": task_id, + "trace_id": span.get_span_context().trace_id, + "span_id": span.get_span_context().span_id, + "thread_id": threading.get_ident() + } + + # Create a parent span + with self.tracer.start_as_current_span("main_task") as main_span: + main_trace_id = main_span.get_span_context().trace_id + + # Execute tasks in thread pool WITHOUT context propagation + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: + futures = [ + executor.submit(worker_task, f"task_{i}") + for i in range(3) + ] + results = [future.result() for future in concurrent.futures.as_completed(futures)] + + spans = self.tester.get_finished_spans() + self.assertEqual(len(spans), 4) # 1 main + 3 worker spans + + # Extract trace IDs from results + worker_trace_ids = [result["trace_id"] for result in results] + + # Each worker should have a different trace ID from the main span + for worker_trace_id in worker_trace_ids: + self.assertNotEqual(worker_trace_id, main_trace_id, + "Worker span should NOT share trace with main span (no context propagation)") + + # Worker spans should also be different from each other (separate traces) + unique_trace_ids = set(worker_trace_ids) + self.assertEqual(len(unique_trace_ids), 3, + "Each worker should create a separate trace") + + # Verify that worker spans have no parent (they are root spans) + worker_spans = [s for s in spans if s.name.startswith("worker_task_")] + for worker_span in worker_spans: + self.assertIsNone(worker_span.parent, + "Worker spans should be root spans without parent") + + def test_threadpool_with_manual_context_propagation_shares_trace(self): + """Test that ThreadPoolExecutor with manual context propagation shares the same trace.""" + def worker_task_with_context(task_info: tuple) -> dict: + """Worker task that restores context before creating spans.""" + task_id, ctx = task_info + + # Restore the context in this thread + token = context.attach(ctx) + try: + with self.tracer.start_as_current_span(f"worker_task_{task_id}") as span: + time.sleep(0.01) # Simulate work + return { + "task_id": task_id, + "trace_id": span.get_span_context().trace_id, + "span_id": span.get_span_context().span_id, + "thread_id": threading.get_ident(), + "parent_span_id": span.parent.span_id if span.parent else None + } + finally: + context.detach(token) + + # Create a parent span and capture its context + with self.tracer.start_as_current_span("main_task") as main_span: + main_trace_id = main_span.get_span_context().trace_id + main_span_id = main_span.get_span_context().span_id + current_context = context.get_current() + + # Execute tasks in thread pool WITH manual context propagation + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: + futures = [ + executor.submit(worker_task_with_context, (f"task_{i}", current_context)) + for i in range(3) + ] + results = [future.result() for future in concurrent.futures.as_completed(futures)] + + spans = self.tester.get_finished_spans() + self.assertEqual(len(spans), 4) # 1 main + 3 worker spans + + # Extract trace IDs from results + worker_trace_ids = [result["trace_id"] for result in results] + + # All workers should share the same trace ID as the main span + for result in results: + self.assertEqual(result["trace_id"], main_trace_id, + f"Worker task {result['task_id']} should share trace with main span") + self.assertEqual(result["parent_span_id"], main_span_id, + f"Worker task {result['task_id']} should have main span as parent") + + # All worker trace IDs should be the same + unique_trace_ids = set(worker_trace_ids) + self.assertEqual(len(unique_trace_ids), 1, + "All workers should share the same trace") + + def test_threadpool_with_contextvars_copy_context_shares_trace(self): + """Test ThreadPoolExecutor with proper context propagation using attach/detach.""" + def worker_task_with_context_management(args) -> dict: + """Worker task that manages context properly.""" + task_id, ctx = args + # Use attach/detach for better control over context + token = context.attach(ctx) + try: + with self.tracer.start_as_current_span(f"worker_task_{task_id}") as span: + time.sleep(0.01) # Simulate work + return { + "task_id": task_id, + "trace_id": span.get_span_context().trace_id, + "span_id": span.get_span_context().span_id, + "thread_id": threading.get_ident(), + "parent_span_id": span.parent.span_id if span.parent else None + } + finally: + context.detach(token) + + # Create a parent span and capture context properly + with self.tracer.start_as_current_span("main_task") as main_span: + main_trace_id = main_span.get_span_context().trace_id + main_span_id = main_span.get_span_context().span_id + + # Get current context to propagate + current_context = context.get_current() + + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: + futures = [ + executor.submit(worker_task_with_context_management, (f"task_{i}", current_context)) + for i in range(3) + ] + results = [future.result() for future in concurrent.futures.as_completed(futures)] + + spans = self.tester.get_finished_spans() + self.assertEqual(len(spans), 4) # 1 main + 3 worker spans + + # All workers should share the same trace ID as the main span + for result in results: + self.assertEqual(result["trace_id"], main_trace_id, + f"Worker task {result['task_id']} should share trace with main span") + self.assertEqual(result["parent_span_id"], main_span_id, + f"Worker task {result['task_id']} should have main span as parent") + + def test_mixed_sequential_and_concurrent_spans(self): + """Test a complex scenario with both sequential and concurrent spans.""" + results = [] + + # Sequential span 1 + trace_id1 = self._create_simple_span("sequential_1") + results.append(("sequential_1", trace_id1)) + + # Concurrent spans with context propagation + with self.tracer.start_as_current_span("concurrent_parent") as parent_span: + parent_trace_id = parent_span.get_span_context().trace_id + results.append(("concurrent_parent", parent_trace_id)) + + def worker_task_with_context(args) -> tuple: + task_id, ctx = args + token = context.attach(ctx) + try: + with self.tracer.start_as_current_span(f"concurrent_{task_id}") as span: + time.sleep(0.01) + return (f"concurrent_{task_id}", span.get_span_context().trace_id) + finally: + context.detach(token) + + current_context = context.get_current() + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + futures = [ + executor.submit(worker_task_with_context, (f"task_{i}", current_context)) + for i in range(2) + ] + concurrent_results = [future.result() for future in concurrent.futures.as_completed(futures)] + results.extend(concurrent_results) + + # Sequential span 2 + trace_id2 = self._create_simple_span("sequential_2") + results.append(("sequential_2", trace_id2)) + + spans = self.tester.get_finished_spans() + self.assertEqual(len(spans), 5) # 2 sequential + 1 parent + 2 concurrent + + # Verify trace relationships + sequential_spans = [r for r in results if r[0].startswith("sequential_")] + concurrent_spans = [r for r in results if r[0].startswith("concurrent_")] + + # Sequential spans should have different traces + sequential_trace_ids = [r[1] for r in sequential_spans] + self.assertEqual(len(set(sequential_trace_ids)), 2, + "Sequential spans should have different traces") + + # Concurrent spans should share the same trace + concurrent_trace_ids = [r[1] for r in concurrent_spans] + unique_concurrent_traces = set(concurrent_trace_ids) + self.assertEqual(len(unique_concurrent_traces), 1, + "All concurrent spans should share the same trace") + + def test_error_handling_in_concurrent_spans(self): + """Test error handling and span status in concurrent execution.""" + def worker_task_with_error_and_context(args) -> dict: + """Worker task that may raise an error.""" + task_id, ctx = args + token = context.attach(ctx) + try: + with self.tracer.start_as_current_span(f"worker_task_{task_id}") as span: + if task_id == "error_task": + span.set_status(trace.Status(trace.StatusCode.ERROR, "Simulated error")) + raise ValueError("Simulated error") + + time.sleep(0.01) + return { + "task_id": task_id, + "trace_id": span.get_span_context().trace_id, + "status": "success" + } + finally: + context.detach(token) + + with self.tracer.start_as_current_span("main_task") as main_span: + main_trace_id = main_span.get_span_context().trace_id + current_context = context.get_current() + + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: + futures = [ + executor.submit(worker_task_with_error_and_context, ("success_task_1", current_context)), + executor.submit(worker_task_with_error_and_context, ("error_task", current_context)), + executor.submit(worker_task_with_error_and_context, ("success_task_2", current_context)), + ] + + results = [] + errors = [] + for future in concurrent.futures.as_completed(futures): + try: + results.append(future.result()) + except Exception as e: + errors.append(str(e)) + + spans = self.tester.get_finished_spans() + self.assertEqual(len(spans), 4) # 1 main + 3 worker spans + + # Should have 2 successful results and 1 error + self.assertEqual(len(results), 2) + self.assertEqual(len(errors), 1) + self.assertIn("Simulated error", errors[0]) + + # All spans should share the same trace + for result in results: + self.assertEqual(result["trace_id"], main_trace_id) + + # Find the error span and verify its status + error_spans = [s for s in spans if s.name == "worker_task_error_task"] + self.assertEqual(len(error_spans), 1) + + error_span = error_spans[0] + self.assertEqual(error_span.status.status_code, trace.StatusCode.ERROR) + + @patch('agentops.sdk.processors.logger') + def test_internal_span_processor_with_concurrent_spans(self, mock_logger): + """Test InternalSpanProcessor behavior with concurrent spans.""" + # Create an InternalSpanProcessor to test + processor = InternalSpanProcessor() + + # Add the processor to the tracer provider + self.tester.tracer_provider.add_span_processor(processor) + + try: + def worker_task_with_context(args) -> str: + task_id, ctx = args + token = context.attach(ctx) + try: + with self.tracer.start_as_current_span(f"openai.chat.completion_{task_id}") as span: + time.sleep(0.01) + return f"result_{task_id}" + finally: + context.detach(token) + + # Execute concurrent tasks + with self.tracer.start_as_current_span("main_session") as main_span: + current_context = context.get_current() + + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + futures = [ + executor.submit(worker_task_with_context, (f"task_{i}", current_context)) + for i in range(2) + ] + results = [future.result() for future in concurrent.futures.as_completed(futures)] + + # Verify results + self.assertEqual(len(results), 2) + + # The processor should have tracked root spans + # Note: With proper context propagation, all spans should belong to the same trace + spans = self.tester.get_finished_spans() + + # Verify that debug logging would have been called + # (The processor tracks root spans and logs when they end) + self.assertTrue(mock_logger.debug.called) + + finally: + # Clean up the processor to avoid affecting other tests + try: + processor.shutdown() + except Exception: + pass + + def test_performance_impact_of_context_propagation(self): + """Test the performance impact of different context propagation methods.""" + import timeit + + def without_context_propagation(): + def worker(): + with self.tracer.start_as_current_span("test_span"): + time.sleep(0.001) + + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + futures = [executor.submit(worker) for _ in range(4)] + [f.result() for f in futures] + + def with_context_propagation(): + def worker_with_context(ctx): + token = context.attach(ctx) + try: + with self.tracer.start_as_current_span("test_span"): + time.sleep(0.001) + finally: + context.detach(token) + + current_context = context.get_current() + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + futures = [executor.submit(worker_with_context, current_context) for _ in range(4)] + [f.result() for f in futures] + + # Clear spans before performance test + self.tester.clear_spans() + + # Measure timing (just to ensure context propagation doesn't break anything) + time_without = timeit.timeit(without_context_propagation, number=1) + self.tester.clear_spans() + + time_with = timeit.timeit(with_context_propagation, number=1) + self.tester.clear_spans() + + # Context propagation should not cause significant performance degradation + # This is a sanity check rather than a strict performance requirement + self.assertGreater(time_with * 10, time_without, + "Context propagation should not cause extreme performance degradation") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From cfb3619d6bea3a08d7da1499179b727e9f3f431f Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Thu, 29 May 2025 19:39:37 +0530 Subject: [PATCH 02/23] ruff checks :) --- agentops/instrumentation/__init__.py | 14 ++-- .../concurrent_futures/__init__.py | 2 +- .../concurrent_futures/instrumentation.py | 72 +++++++++---------- agentops/sdk/decorators/__init__.py | 1 + 4 files changed, 45 insertions(+), 44 deletions(-) diff --git a/agentops/instrumentation/__init__.py b/agentops/instrumentation/__init__.py index 121754933..54e4b1c10 100644 --- a/agentops/instrumentation/__init__.py +++ b/agentops/instrumentation/__init__.py @@ -69,7 +69,7 @@ def _should_instrument_package(package_name: str) -> bool: Handles special cases for agentic libraries, providers, and utility instrumentors. """ global _has_agentic_library - + # If this is an agentic library, uninstrument all providers first if package_name in AGENTIC_LIBRARIES: _uninstrument_providers() @@ -79,7 +79,7 @@ def _should_instrument_package(package_name: str) -> bool: # Skip providers if an agentic library is already instrumented if package_name in PROVIDERS and _has_agentic_library: return False - + # Utility instrumentors are always enabled regardless of agentic library state if package_name in UTILITY_INSTRUMENTORS: return not _is_package_instrumented(package_name) @@ -98,10 +98,12 @@ def _perform_instrumentation(package_name: str): return # Get the appropriate configuration for the package - config = PROVIDERS.get(package_name) or AGENTIC_LIBRARIES.get(package_name) or UTILITY_INSTRUMENTORS.get(package_name) + config = ( + PROVIDERS.get(package_name) or AGENTIC_LIBRARIES.get(package_name) or UTILITY_INSTRUMENTORS.get(package_name) + ) if not config: return - + loader = InstrumentorLoader(**config) if loader.should_activate: @@ -151,7 +153,6 @@ def _import_monitor(name: str, globals_dict=None, locals_dict=None, fromlist=(), # Instrument all matching packages for package_to_check in packages_to_check: if package_to_check not in _instrumenting_packages and not _is_package_instrumented(package_to_check): - _instrumenting_packages.add(package_to_check) try: _perform_instrumentation(package_to_check) @@ -266,9 +267,10 @@ def should_activate(self) -> bool: # Special case for stdlib modules (like concurrent.futures) if self.package_name == "python": import sys + python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" return Version(python_version) >= parse(self.min_version) - + # Use explicit package_name if provided, otherwise derive from module_name if self.package_name: provider_name = self.package_name diff --git a/agentops/instrumentation/concurrent_futures/__init__.py b/agentops/instrumentation/concurrent_futures/__init__.py index 716b95f3f..943fd5b0b 100644 --- a/agentops/instrumentation/concurrent_futures/__init__.py +++ b/agentops/instrumentation/concurrent_futures/__init__.py @@ -7,4 +7,4 @@ from .instrumentation import ConcurrentFuturesInstrumentor -__all__ = ["ConcurrentFuturesInstrumentor"] \ No newline at end of file +__all__ = ["ConcurrentFuturesInstrumentor"] diff --git a/agentops/instrumentation/concurrent_futures/instrumentation.py b/agentops/instrumentation/concurrent_futures/instrumentation.py index bbd468e31..3ed4caee3 100644 --- a/agentops/instrumentation/concurrent_futures/instrumentation.py +++ b/agentops/instrumentation/concurrent_futures/instrumentation.py @@ -7,12 +7,10 @@ import contextvars import functools -import sys -from typing import Collection, Optional, Any, Callable +from typing import Collection from concurrent.futures import ThreadPoolExecutor from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.util._importlib_metadata import version from agentops.logging import logger @@ -23,23 +21,23 @@ def _context_propagating_init(original_init): """Wrap ThreadPoolExecutor.__init__ to set up context-aware initializer.""" - + @functools.wraps(original_init) - def wrapped_init(self, max_workers=None, thread_name_prefix='', initializer=None, initargs=()): + def wrapped_init(self, max_workers=None, thread_name_prefix="", initializer=None, initargs=()): # Capture the current context when the executor is created main_context = contextvars.copy_context() - + def context_aware_initializer(): """Initializer that sets up the captured context in each worker thread.""" logger.debug("[ConcurrentFuturesInstrumentor] Setting up context in worker thread") - + # Set the main context variables in this thread for var, value in main_context.items(): try: var.set(value) except Exception as e: logger.debug(f"[ConcurrentFuturesInstrumentor] Could not set context var {var}: {e}") - + # Run user's initializer if provided if initializer and callable(initializer): try: @@ -50,93 +48,93 @@ def context_aware_initializer(): except Exception as e: logger.error(f"[ConcurrentFuturesInstrumentor] Error in user initializer: {e}") raise - + logger.debug("[ConcurrentFuturesInstrumentor] Worker thread context setup complete") - + # Create executor with context-aware initializer - prefix = f'AgentOps-{thread_name_prefix}' if thread_name_prefix else 'AgentOps-Thread' - + prefix = f"AgentOps-{thread_name_prefix}" if thread_name_prefix else "AgentOps-Thread" + # Call original init with our context-aware initializer original_init( self, max_workers=max_workers, thread_name_prefix=prefix, initializer=context_aware_initializer, - initargs=() # We handle initargs in our wrapper + initargs=(), # We handle initargs in our wrapper ) - - logger.debug(f"[ConcurrentFuturesInstrumentor] ThreadPoolExecutor initialized with context propagation") - + + logger.debug("[ConcurrentFuturesInstrumentor] ThreadPoolExecutor initialized with context propagation") + return wrapped_init def _context_propagating_submit(original_submit): """Wrap ThreadPoolExecutor.submit to ensure context propagation.""" - + @functools.wraps(original_submit) def wrapped_submit(self, func, *args, **kwargs): # Log the submission - func_name = getattr(func, '__name__', str(func)) + func_name = getattr(func, "__name__", str(func)) logger.debug(f"[ConcurrentFuturesInstrumentor] Submitting function: {func_name}") - + # The context propagation is handled by the initializer, so we can submit normally # But we can add additional logging or monitoring here if needed return original_submit(self, func, *args, **kwargs) - + return wrapped_submit class ConcurrentFuturesInstrumentor(BaseInstrumentor): """ Instrumentor for concurrent.futures module. - + This instrumentor patches ThreadPoolExecutor to automatically propagate OpenTelemetry context to worker threads, ensuring all LLM calls and other instrumented operations maintain proper trace context. """ - + def instrumentation_dependencies(self) -> Collection[str]: """Return a list of instrumentation dependencies.""" return [] - + def _instrument(self, **kwargs): """Instrument the concurrent.futures module.""" global _original_init, _original_submit - + logger.debug("[ConcurrentFuturesInstrumentor] Starting instrumentation") - + # Store original methods _original_init = ThreadPoolExecutor.__init__ _original_submit = ThreadPoolExecutor.submit - + # Patch ThreadPoolExecutor methods ThreadPoolExecutor.__init__ = _context_propagating_init(_original_init) ThreadPoolExecutor.submit = _context_propagating_submit(_original_submit) - + logger.info("[ConcurrentFuturesInstrumentor] Successfully instrumented concurrent.futures.ThreadPoolExecutor") - + def _uninstrument(self, **kwargs): """Uninstrument the concurrent.futures module.""" global _original_init, _original_submit - + logger.debug("[ConcurrentFuturesInstrumentor] Starting uninstrumentation") - + # Restore original methods if _original_init: ThreadPoolExecutor.__init__ = _original_init _original_init = None - + if _original_submit: ThreadPoolExecutor.submit = _original_submit _original_submit = None - + logger.info("[ConcurrentFuturesInstrumentor] Successfully uninstrumented concurrent.futures.ThreadPoolExecutor") - + @staticmethod def instrument_module_directly(): """ Directly instrument the module without using the standard instrumentor interface. - + This can be called manually if automatic instrumentation is not desired. """ instrumentor = ConcurrentFuturesInstrumentor() @@ -144,16 +142,16 @@ def instrument_module_directly(): instrumentor.instrument() return True return False - + @staticmethod def uninstrument_module_directly(): """ Directly uninstrument the module. - + This can be called manually to remove instrumentation. """ instrumentor = ConcurrentFuturesInstrumentor() if instrumentor.is_instrumented_by_opentelemetry: instrumentor.uninstrument() return True - return False \ No newline at end of file + return False diff --git a/agentops/sdk/decorators/__init__.py b/agentops/sdk/decorators/__init__.py index f775b45d5..608b23908 100644 --- a/agentops/sdk/decorators/__init__.py +++ b/agentops/sdk/decorators/__init__.py @@ -20,6 +20,7 @@ tool = create_entity_decorator(SpanKind.TOOL) operation = task + # For backward compatibility: @session decorator calls @trace decorator @functools.wraps(trace) def session(*args, **kwargs): From ea8401fffc1ac204c500cf1711a52a48a1550ba5 Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Thu, 29 May 2025 19:40:49 +0530 Subject: [PATCH 03/23] ruff check again :( --- .../sdk/test_concurrent_instrumentation.py | 179 +++++++++--------- 1 file changed, 85 insertions(+), 94 deletions(-) diff --git a/tests/unit/sdk/test_concurrent_instrumentation.py b/tests/unit/sdk/test_concurrent_instrumentation.py index 0dd2ac93b..d2f2359ba 100644 --- a/tests/unit/sdk/test_concurrent_instrumentation.py +++ b/tests/unit/sdk/test_concurrent_instrumentation.py @@ -6,14 +6,13 @@ """ import concurrent.futures -import contextvars import time import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import patch import threading from opentelemetry import context, trace -from opentelemetry.sdk.trace import ReadableSpan, TracerProvider +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter @@ -23,11 +22,11 @@ class IsolatedInstrumentationTester: """ A lighter-weight instrumentation tester that doesn't affect global state. - + This version creates an isolated tracer provider and doesn't shut down the global tracing core, making it safer for use alongside other tests. """ - + def __init__(self): """Initialize with isolated tracer provider.""" # Create isolated tracer provider and exporter @@ -35,19 +34,19 @@ def __init__(self): self.memory_exporter = InMemorySpanExporter() self.span_processor = SimpleSpanProcessor(self.memory_exporter) self.tracer_provider.add_span_processor(self.span_processor) - + # Don't set as global provider - keep isolated self.tracer = self.tracer_provider.get_tracer(__name__) - + def get_tracer(self): """Get the isolated tracer.""" return self.tracer - + def clear_spans(self): """Clear all spans from the memory exporter.""" self.span_processor.force_flush() self.memory_exporter.clear() - + def get_finished_spans(self): """Get all finished spans.""" self.span_processor.force_flush() @@ -78,22 +77,20 @@ def _create_nested_spans(self, parent_name: str, child_name: str) -> tuple: with self.tracer.start_as_current_span(parent_name) as parent_span: parent_trace_id = parent_span.get_span_context().trace_id time.sleep(0.01) - + with self.tracer.start_as_current_span(child_name) as child_span: child_trace_id = child_span.get_span_context().trace_id time.sleep(0.01) - + return parent_trace_id, child_trace_id def test_sequential_spans_same_trace(self): """Test that sequential spans in the same thread share the same trace.""" - trace_id1 = self._create_simple_span("span1") - trace_id2 = self._create_simple_span("span2") - + # In sequential execution, spans should be independent (different traces) spans = self.tester.get_finished_spans() self.assertEqual(len(spans), 2) - + # Each span should be a root span (no parent) for span in spans: self.assertIsNone(span.parent) @@ -101,28 +98,29 @@ def test_sequential_spans_same_trace(self): def test_nested_spans_same_trace(self): """Test that nested spans share the same trace.""" parent_trace_id, child_trace_id = self._create_nested_spans("parent", "child") - + # Nested spans should share the same trace self.assertEqual(parent_trace_id, child_trace_id) - + spans = self.tester.get_finished_spans() self.assertEqual(len(spans), 2) - + # Find parent and child spans parent_spans = [s for s in spans if s.name == "parent"] child_spans = [s for s in spans if s.name == "child"] - + self.assertEqual(len(parent_spans), 1) self.assertEqual(len(child_spans), 1) - + parent_span = parent_spans[0] child_span = child_spans[0] - + # Child should have parent as its parent self.assertEqual(child_span.parent.span_id, parent_span.context.span_id) def test_threadpool_without_context_propagation_creates_separate_traces(self): """Test that ThreadPoolExecutor without context propagation creates separate traces.""" + def worker_task(task_id: str) -> dict: """Worker task that creates a span without context propagation.""" with self.tracer.start_as_current_span(f"worker_task_{task_id}") as span: @@ -131,19 +129,16 @@ def worker_task(task_id: str) -> dict: "task_id": task_id, "trace_id": span.get_span_context().trace_id, "span_id": span.get_span_context().span_id, - "thread_id": threading.get_ident() + "thread_id": threading.get_ident(), } # Create a parent span with self.tracer.start_as_current_span("main_task") as main_span: main_trace_id = main_span.get_span_context().trace_id - + # Execute tasks in thread pool WITHOUT context propagation with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: - futures = [ - executor.submit(worker_task, f"task_{i}") - for i in range(3) - ] + futures = [executor.submit(worker_task, f"task_{i}") for i in range(3)] results = [future.result() for future in concurrent.futures.as_completed(futures)] spans = self.tester.get_finished_spans() @@ -151,29 +146,31 @@ def worker_task(task_id: str) -> dict: # Extract trace IDs from results worker_trace_ids = [result["trace_id"] for result in results] - + # Each worker should have a different trace ID from the main span for worker_trace_id in worker_trace_ids: - self.assertNotEqual(worker_trace_id, main_trace_id, - "Worker span should NOT share trace with main span (no context propagation)") + self.assertNotEqual( + worker_trace_id, + main_trace_id, + "Worker span should NOT share trace with main span (no context propagation)", + ) # Worker spans should also be different from each other (separate traces) unique_trace_ids = set(worker_trace_ids) - self.assertEqual(len(unique_trace_ids), 3, - "Each worker should create a separate trace") + self.assertEqual(len(unique_trace_ids), 3, "Each worker should create a separate trace") # Verify that worker spans have no parent (they are root spans) worker_spans = [s for s in spans if s.name.startswith("worker_task_")] for worker_span in worker_spans: - self.assertIsNone(worker_span.parent, - "Worker spans should be root spans without parent") + self.assertIsNone(worker_span.parent, "Worker spans should be root spans without parent") def test_threadpool_with_manual_context_propagation_shares_trace(self): """Test that ThreadPoolExecutor with manual context propagation shares the same trace.""" + def worker_task_with_context(task_info: tuple) -> dict: """Worker task that restores context before creating spans.""" task_id, ctx = task_info - + # Restore the context in this thread token = context.attach(ctx) try: @@ -184,7 +181,7 @@ def worker_task_with_context(task_info: tuple) -> dict: "trace_id": span.get_span_context().trace_id, "span_id": span.get_span_context().span_id, "thread_id": threading.get_ident(), - "parent_span_id": span.parent.span_id if span.parent else None + "parent_span_id": span.parent.span_id if span.parent else None, } finally: context.detach(token) @@ -194,13 +191,10 @@ def worker_task_with_context(task_info: tuple) -> dict: main_trace_id = main_span.get_span_context().trace_id main_span_id = main_span.get_span_context().span_id current_context = context.get_current() - + # Execute tasks in thread pool WITH manual context propagation with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: - futures = [ - executor.submit(worker_task_with_context, (f"task_{i}", current_context)) - for i in range(3) - ] + futures = [executor.submit(worker_task_with_context, (f"task_{i}", current_context)) for i in range(3)] results = [future.result() for future in concurrent.futures.as_completed(futures)] spans = self.tester.get_finished_spans() @@ -208,21 +202,25 @@ def worker_task_with_context(task_info: tuple) -> dict: # Extract trace IDs from results worker_trace_ids = [result["trace_id"] for result in results] - + # All workers should share the same trace ID as the main span for result in results: - self.assertEqual(result["trace_id"], main_trace_id, - f"Worker task {result['task_id']} should share trace with main span") - self.assertEqual(result["parent_span_id"], main_span_id, - f"Worker task {result['task_id']} should have main span as parent") + self.assertEqual( + result["trace_id"], main_trace_id, f"Worker task {result['task_id']} should share trace with main span" + ) + self.assertEqual( + result["parent_span_id"], + main_span_id, + f"Worker task {result['task_id']} should have main span as parent", + ) # All worker trace IDs should be the same unique_trace_ids = set(worker_trace_ids) - self.assertEqual(len(unique_trace_ids), 1, - "All workers should share the same trace") + self.assertEqual(len(unique_trace_ids), 1, "All workers should share the same trace") def test_threadpool_with_contextvars_copy_context_shares_trace(self): """Test ThreadPoolExecutor with proper context propagation using attach/detach.""" + def worker_task_with_context_management(args) -> dict: """Worker task that manages context properly.""" task_id, ctx = args @@ -236,7 +234,7 @@ def worker_task_with_context_management(args) -> dict: "trace_id": span.get_span_context().trace_id, "span_id": span.get_span_context().span_id, "thread_id": threading.get_ident(), - "parent_span_id": span.parent.span_id if span.parent else None + "parent_span_id": span.parent.span_id if span.parent else None, } finally: context.detach(token) @@ -245,10 +243,10 @@ def worker_task_with_context_management(args) -> dict: with self.tracer.start_as_current_span("main_task") as main_span: main_trace_id = main_span.get_span_context().trace_id main_span_id = main_span.get_span_context().span_id - + # Get current context to propagate current_context = context.get_current() - + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: futures = [ executor.submit(worker_task_with_context_management, (f"task_{i}", current_context)) @@ -261,10 +259,14 @@ def worker_task_with_context_management(args) -> dict: # All workers should share the same trace ID as the main span for result in results: - self.assertEqual(result["trace_id"], main_trace_id, - f"Worker task {result['task_id']} should share trace with main span") - self.assertEqual(result["parent_span_id"], main_span_id, - f"Worker task {result['task_id']} should have main span as parent") + self.assertEqual( + result["trace_id"], main_trace_id, f"Worker task {result['task_id']} should share trace with main span" + ) + self.assertEqual( + result["parent_span_id"], + main_span_id, + f"Worker task {result['task_id']} should have main span as parent", + ) def test_mixed_sequential_and_concurrent_spans(self): """Test a complex scenario with both sequential and concurrent spans.""" @@ -291,10 +293,7 @@ def worker_task_with_context(args) -> tuple: current_context = context.get_current() with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: - futures = [ - executor.submit(worker_task_with_context, (f"task_{i}", current_context)) - for i in range(2) - ] + futures = [executor.submit(worker_task_with_context, (f"task_{i}", current_context)) for i in range(2)] concurrent_results = [future.result() for future in concurrent.futures.as_completed(futures)] results.extend(concurrent_results) @@ -311,17 +310,16 @@ def worker_task_with_context(args) -> tuple: # Sequential spans should have different traces sequential_trace_ids = [r[1] for r in sequential_spans] - self.assertEqual(len(set(sequential_trace_ids)), 2, - "Sequential spans should have different traces") + self.assertEqual(len(set(sequential_trace_ids)), 2, "Sequential spans should have different traces") # Concurrent spans should share the same trace concurrent_trace_ids = [r[1] for r in concurrent_spans] unique_concurrent_traces = set(concurrent_trace_ids) - self.assertEqual(len(unique_concurrent_traces), 1, - "All concurrent spans should share the same trace") + self.assertEqual(len(unique_concurrent_traces), 1, "All concurrent spans should share the same trace") def test_error_handling_in_concurrent_spans(self): """Test error handling and span status in concurrent execution.""" + def worker_task_with_error_and_context(args) -> dict: """Worker task that may raise an error.""" task_id, ctx = args @@ -331,13 +329,9 @@ def worker_task_with_error_and_context(args) -> dict: if task_id == "error_task": span.set_status(trace.Status(trace.StatusCode.ERROR, "Simulated error")) raise ValueError("Simulated error") - + time.sleep(0.01) - return { - "task_id": task_id, - "trace_id": span.get_span_context().trace_id, - "status": "success" - } + return {"task_id": task_id, "trace_id": span.get_span_context().trace_id, "status": "success"} finally: context.detach(token) @@ -351,7 +345,7 @@ def worker_task_with_error_and_context(args) -> dict: executor.submit(worker_task_with_error_and_context, ("error_task", current_context)), executor.submit(worker_task_with_error_and_context, ("success_task_2", current_context)), ] - + results = [] errors = [] for future in concurrent.futures.as_completed(futures): @@ -375,52 +369,48 @@ def worker_task_with_error_and_context(args) -> dict: # Find the error span and verify its status error_spans = [s for s in spans if s.name == "worker_task_error_task"] self.assertEqual(len(error_spans), 1) - + error_span = error_spans[0] self.assertEqual(error_span.status.status_code, trace.StatusCode.ERROR) - @patch('agentops.sdk.processors.logger') + @patch("agentops.sdk.processors.logger") def test_internal_span_processor_with_concurrent_spans(self, mock_logger): """Test InternalSpanProcessor behavior with concurrent spans.""" # Create an InternalSpanProcessor to test processor = InternalSpanProcessor() - + # Add the processor to the tracer provider self.tester.tracer_provider.add_span_processor(processor) - + try: + def worker_task_with_context(args) -> str: task_id, ctx = args token = context.attach(ctx) try: - with self.tracer.start_as_current_span(f"openai.chat.completion_{task_id}") as span: + with self.tracer.start_as_current_span(f"openai.chat.completion_{task_id}"): time.sleep(0.01) return f"result_{task_id}" finally: context.detach(token) # Execute concurrent tasks - with self.tracer.start_as_current_span("main_session") as main_span: + with self.tracer.start_as_current_span("main_session"): current_context = context.get_current() - + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: futures = [ - executor.submit(worker_task_with_context, (f"task_{i}", current_context)) - for i in range(2) + executor.submit(worker_task_with_context, (f"task_{i}", current_context)) for i in range(2) ] results = [future.result() for future in concurrent.futures.as_completed(futures)] # Verify results self.assertEqual(len(results), 2) - - # The processor should have tracked root spans - # Note: With proper context propagation, all spans should belong to the same trace - spans = self.tester.get_finished_spans() - + # Verify that debug logging would have been called # (The processor tracks root spans and logs when they end) self.assertTrue(mock_logger.debug.called) - + finally: # Clean up the processor to avoid affecting other tests try: @@ -431,12 +421,12 @@ def worker_task_with_context(args) -> str: def test_performance_impact_of_context_propagation(self): """Test the performance impact of different context propagation methods.""" import timeit - + def without_context_propagation(): def worker(): with self.tracer.start_as_current_span("test_span"): time.sleep(0.001) - + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: futures = [executor.submit(worker) for _ in range(4)] [f.result() for f in futures] @@ -449,7 +439,7 @@ def worker_with_context(ctx): time.sleep(0.001) finally: context.detach(token) - + current_context = context.get_current() with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: futures = [executor.submit(worker_with_context, current_context) for _ in range(4)] @@ -457,19 +447,20 @@ def worker_with_context(ctx): # Clear spans before performance test self.tester.clear_spans() - + # Measure timing (just to ensure context propagation doesn't break anything) time_without = timeit.timeit(without_context_propagation, number=1) self.tester.clear_spans() - + time_with = timeit.timeit(with_context_propagation, number=1) self.tester.clear_spans() - + # Context propagation should not cause significant performance degradation # This is a sanity check rather than a strict performance requirement - self.assertGreater(time_with * 10, time_without, - "Context propagation should not cause extreme performance degradation") + self.assertGreater( + time_with * 10, time_without, "Context propagation should not cause extreme performance degradation" + ) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From d17f14e77356b289bbdc34ebaf4507d88564f429 Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Thu, 29 May 2025 19:46:15 +0530 Subject: [PATCH 04/23] damn ruff +_+ --- tests/unit/sdk/test_concurrent_instrumentation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/sdk/test_concurrent_instrumentation.py b/tests/unit/sdk/test_concurrent_instrumentation.py index d2f2359ba..a702a1307 100644 --- a/tests/unit/sdk/test_concurrent_instrumentation.py +++ b/tests/unit/sdk/test_concurrent_instrumentation.py @@ -86,6 +86,9 @@ def _create_nested_spans(self, parent_name: str, child_name: str) -> tuple: def test_sequential_spans_same_trace(self): """Test that sequential spans in the same thread share the same trace.""" + self._create_simple_span("span1") + self._create_simple_span("span2") + # In sequential execution, spans should be independent (different traces) spans = self.tester.get_finished_spans() From 4c2faa2d74b73faa70f13837dec0405d7cc44788 Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Thu, 29 May 2025 19:48:16 +0530 Subject: [PATCH 05/23] heck ruff again --- tests/unit/sdk/test_concurrent_instrumentation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/sdk/test_concurrent_instrumentation.py b/tests/unit/sdk/test_concurrent_instrumentation.py index a702a1307..14f4114f4 100644 --- a/tests/unit/sdk/test_concurrent_instrumentation.py +++ b/tests/unit/sdk/test_concurrent_instrumentation.py @@ -88,7 +88,6 @@ def test_sequential_spans_same_trace(self): """Test that sequential spans in the same thread share the same trace.""" self._create_simple_span("span1") self._create_simple_span("span2") - # In sequential execution, spans should be independent (different traces) spans = self.tester.get_finished_spans() From 791f6b9c1d9d02dfb732eb05c7fc643aa5f555c1 Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Sun, 1 Jun 2025 04:03:42 +0530 Subject: [PATCH 06/23] Added Mem0 and Concurrent Futures Instrumentation(for testing pr is already raised for that) --- agentops/instrumentation/__init__.py | 39 +- .../concurrent_futures/__init__.py | 10 + .../concurrent_futures/instrumentation.py | 157 +++++++ agentops/instrumentation/mem0/__init__.py | 53 +++ agentops/instrumentation/mem0/common.py | 377 +++++++++++++++ agentops/instrumentation/mem0/instrumentor.py | 283 ++++++++++++ agentops/instrumentation/mem0/memory.py | 430 ++++++++++++++++++ 7 files changed, 1346 insertions(+), 3 deletions(-) create mode 100644 agentops/instrumentation/concurrent_futures/__init__.py create mode 100644 agentops/instrumentation/concurrent_futures/instrumentation.py create mode 100644 agentops/instrumentation/mem0/__init__.py create mode 100644 agentops/instrumentation/mem0/common.py create mode 100644 agentops/instrumentation/mem0/instrumentor.py create mode 100644 agentops/instrumentation/mem0/memory.py diff --git a/agentops/instrumentation/__init__.py b/agentops/instrumentation/__init__.py index b7916e62a..2ee75db0a 100644 --- a/agentops/instrumentation/__init__.py +++ b/agentops/instrumentation/__init__.py @@ -66,9 +66,10 @@ def _uninstrument_providers(): def _should_instrument_package(package_name: str) -> bool: """ Determine if a package should be instrumented based on current state. - Handles special cases for agentic libraries and providers. + Handles special cases for agentic libraries, providers, and utility instrumentors. """ global _has_agentic_library + # If this is an agentic library, uninstrument all providers first if package_name in AGENTIC_LIBRARIES: _uninstrument_providers() @@ -79,6 +80,10 @@ def _should_instrument_package(package_name: str) -> bool: if package_name in PROVIDERS and _has_agentic_library: return False + # Utility instrumentors are always enabled regardless of agentic library state + if package_name in UTILITY_INSTRUMENTORS: + return not _is_package_instrumented(package_name) + # Skip if already instrumented if _is_package_instrumented(package_name): return False @@ -93,7 +98,12 @@ def _perform_instrumentation(package_name: str): return # Get the appropriate configuration for the package - config = PROVIDERS.get(package_name) or AGENTIC_LIBRARIES[package_name] + config = ( + PROVIDERS.get(package_name) or AGENTIC_LIBRARIES.get(package_name) or UTILITY_INSTRUMENTORS.get(package_name) + ) + if not config: + return + loader = InstrumentorLoader(**config) if loader.should_activate: @@ -188,6 +198,22 @@ class InstrumentorConfig(TypedDict): "min_version": "0.1.0", "package_name": "google-genai", # Actual pip package name }, + "mem0": { + "module_name": "agentops.instrumentation.mem0", + "class_name": "Mem0Instrumentor", + "min_version": "0.1.10", + "package_name": "mem0ai", # Actual pip package name + }, +} + +# Configuration for utility instrumentors +UTILITY_INSTRUMENTORS: dict[str, InstrumentorConfig] = { + "concurrent.futures": { + "module_name": "agentops.instrumentation.concurrent_futures", + "class_name": "ConcurrentFuturesInstrumentor", + "min_version": "3.7.0", # Python 3.7+ (concurrent.futures is stdlib) + "package_name": "python", # Special case for stdlib modules + }, } # Configuration for supported agentic libraries @@ -211,7 +237,7 @@ class InstrumentorConfig(TypedDict): } # Combine all target packages for monitoring -TARGET_PACKAGES = set(PROVIDERS.keys()) | set(AGENTIC_LIBRARIES.keys()) +TARGET_PACKAGES = set(PROVIDERS.keys()) | set(AGENTIC_LIBRARIES.keys()) | set(UTILITY_INSTRUMENTORS.keys()) # Create a single instance of the manager # _manager = InstrumentationManager() # Removed @@ -238,6 +264,13 @@ def module(self) -> ModuleType: def should_activate(self) -> bool: """Check if the package is available and meets version requirements.""" try: + # Special case for stdlib modules (like concurrent.futures) + if self.package_name == "python": + import sys + + python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + return Version(python_version) >= parse(self.min_version) + # Use explicit package_name if provided, otherwise derive from module_name if self.package_name: provider_name = self.package_name diff --git a/agentops/instrumentation/concurrent_futures/__init__.py b/agentops/instrumentation/concurrent_futures/__init__.py new file mode 100644 index 000000000..943fd5b0b --- /dev/null +++ b/agentops/instrumentation/concurrent_futures/__init__.py @@ -0,0 +1,10 @@ +""" +Instrumentation for concurrent.futures module. + +This module provides automatic instrumentation for ThreadPoolExecutor to ensure +proper OpenTelemetry context propagation across thread boundaries. +""" + +from .instrumentation import ConcurrentFuturesInstrumentor + +__all__ = ["ConcurrentFuturesInstrumentor"] diff --git a/agentops/instrumentation/concurrent_futures/instrumentation.py b/agentops/instrumentation/concurrent_futures/instrumentation.py new file mode 100644 index 000000000..3ed4caee3 --- /dev/null +++ b/agentops/instrumentation/concurrent_futures/instrumentation.py @@ -0,0 +1,157 @@ +""" +OpenTelemetry Instrumentation for concurrent.futures module. + +This instrumentation automatically patches ThreadPoolExecutor to ensure proper +context propagation across thread boundaries, preventing "NEW TRACE DETECTED" issues. +""" + +import contextvars +import functools +from typing import Collection +from concurrent.futures import ThreadPoolExecutor + +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor + +from agentops.logging import logger + +# Store original methods to restore during uninstrumentation +_original_init = None +_original_submit = None + + +def _context_propagating_init(original_init): + """Wrap ThreadPoolExecutor.__init__ to set up context-aware initializer.""" + + @functools.wraps(original_init) + def wrapped_init(self, max_workers=None, thread_name_prefix="", initializer=None, initargs=()): + # Capture the current context when the executor is created + main_context = contextvars.copy_context() + + def context_aware_initializer(): + """Initializer that sets up the captured context in each worker thread.""" + logger.debug("[ConcurrentFuturesInstrumentor] Setting up context in worker thread") + + # Set the main context variables in this thread + for var, value in main_context.items(): + try: + var.set(value) + except Exception as e: + logger.debug(f"[ConcurrentFuturesInstrumentor] Could not set context var {var}: {e}") + + # Run user's initializer if provided + if initializer and callable(initializer): + try: + if initargs: + initializer(*initargs) + else: + initializer() + except Exception as e: + logger.error(f"[ConcurrentFuturesInstrumentor] Error in user initializer: {e}") + raise + + logger.debug("[ConcurrentFuturesInstrumentor] Worker thread context setup complete") + + # Create executor with context-aware initializer + prefix = f"AgentOps-{thread_name_prefix}" if thread_name_prefix else "AgentOps-Thread" + + # Call original init with our context-aware initializer + original_init( + self, + max_workers=max_workers, + thread_name_prefix=prefix, + initializer=context_aware_initializer, + initargs=(), # We handle initargs in our wrapper + ) + + logger.debug("[ConcurrentFuturesInstrumentor] ThreadPoolExecutor initialized with context propagation") + + return wrapped_init + + +def _context_propagating_submit(original_submit): + """Wrap ThreadPoolExecutor.submit to ensure context propagation.""" + + @functools.wraps(original_submit) + def wrapped_submit(self, func, *args, **kwargs): + # Log the submission + func_name = getattr(func, "__name__", str(func)) + logger.debug(f"[ConcurrentFuturesInstrumentor] Submitting function: {func_name}") + + # The context propagation is handled by the initializer, so we can submit normally + # But we can add additional logging or monitoring here if needed + return original_submit(self, func, *args, **kwargs) + + return wrapped_submit + + +class ConcurrentFuturesInstrumentor(BaseInstrumentor): + """ + Instrumentor for concurrent.futures module. + + This instrumentor patches ThreadPoolExecutor to automatically propagate + OpenTelemetry context to worker threads, ensuring all LLM calls and other + instrumented operations maintain proper trace context. + """ + + def instrumentation_dependencies(self) -> Collection[str]: + """Return a list of instrumentation dependencies.""" + return [] + + def _instrument(self, **kwargs): + """Instrument the concurrent.futures module.""" + global _original_init, _original_submit + + logger.debug("[ConcurrentFuturesInstrumentor] Starting instrumentation") + + # Store original methods + _original_init = ThreadPoolExecutor.__init__ + _original_submit = ThreadPoolExecutor.submit + + # Patch ThreadPoolExecutor methods + ThreadPoolExecutor.__init__ = _context_propagating_init(_original_init) + ThreadPoolExecutor.submit = _context_propagating_submit(_original_submit) + + logger.info("[ConcurrentFuturesInstrumentor] Successfully instrumented concurrent.futures.ThreadPoolExecutor") + + def _uninstrument(self, **kwargs): + """Uninstrument the concurrent.futures module.""" + global _original_init, _original_submit + + logger.debug("[ConcurrentFuturesInstrumentor] Starting uninstrumentation") + + # Restore original methods + if _original_init: + ThreadPoolExecutor.__init__ = _original_init + _original_init = None + + if _original_submit: + ThreadPoolExecutor.submit = _original_submit + _original_submit = None + + logger.info("[ConcurrentFuturesInstrumentor] Successfully uninstrumented concurrent.futures.ThreadPoolExecutor") + + @staticmethod + def instrument_module_directly(): + """ + Directly instrument the module without using the standard instrumentor interface. + + This can be called manually if automatic instrumentation is not desired. + """ + instrumentor = ConcurrentFuturesInstrumentor() + if not instrumentor.is_instrumented_by_opentelemetry: + instrumentor.instrument() + return True + return False + + @staticmethod + def uninstrument_module_directly(): + """ + Directly uninstrument the module. + + This can be called manually to remove instrumentation. + """ + instrumentor = ConcurrentFuturesInstrumentor() + if instrumentor.is_instrumented_by_opentelemetry: + instrumentor.uninstrument() + return True + return False diff --git a/agentops/instrumentation/mem0/__init__.py b/agentops/instrumentation/mem0/__init__.py new file mode 100644 index 000000000..ababf20c9 --- /dev/null +++ b/agentops/instrumentation/mem0/__init__.py @@ -0,0 +1,53 @@ +"""Mem0 instrumentation library for AgentOps. + +This package provides instrumentation for the Mem0 memory management system, +capturing telemetry data for memory operations. +""" + +import logging + +# Import memory operation wrappers +from .memory import ( + mem0_add_wrapper, + mem0_search_wrapper, + mem0_get_all_wrapper, + mem0_get_wrapper, + mem0_delete_wrapper, + mem0_update_wrapper, + mem0_delete_all_wrapper, + mem0_history_wrapper, +) + + +def get_version() -> str: + try: + from importlib.metadata import version + + return version("mem0ai") + except ImportError: + logger.debug("Could not find Mem0 SDK version") + return "unknown" + + +LIBRARY_NAME = "agentops.instrumentation.mem0" +LIBRARY_VERSION = "1.0.0" + +logger = logging.getLogger(__name__) + +# Import after defining constants to avoid circular imports +from agentops.instrumentation.mem0.instrumentor import Mem0Instrumentor # noqa: E402 + +__all__ = [ + "LIBRARY_NAME", + "LIBRARY_VERSION", + "Mem0Instrumentor", + # Memory operation wrappers + "mem0_add_wrapper", + "mem0_search_wrapper", + "mem0_get_all_wrapper", + "mem0_get_wrapper", + "mem0_delete_wrapper", + "mem0_update_wrapper", + "mem0_delete_all_wrapper", + "mem0_history_wrapper", +] diff --git a/agentops/instrumentation/mem0/common.py b/agentops/instrumentation/mem0/common.py new file mode 100644 index 000000000..55576f78d --- /dev/null +++ b/agentops/instrumentation/mem0/common.py @@ -0,0 +1,377 @@ +"""Common utilities and base wrapper functions for Mem0 instrumentation.""" + +from typing import Dict, Any +from opentelemetry import context as context_api +from opentelemetry.trace import SpanKind, Status, StatusCode + +from agentops.instrumentation.common.attributes import AttributeMap +from agentops.semconv import SpanAttributes, LLMRequestTypeValues + + +def get_common_mem0_attributes() -> AttributeMap: + """Get common instrumentation attributes for Mem0 operations. + + Returns: + Dictionary of common Mem0 attributes + """ + attributes = {} + attributes[SpanAttributes.LLM_SYSTEM] = "Mem0" + return attributes + + +def _extract_common_kwargs_attributes(kwargs: Dict[str, Any]) -> AttributeMap: + """Extract common attributes from kwargs that apply to multiple operations. + + Args: + kwargs: Keyword arguments from the method call + + Returns: + Dictionary of extracted common attributes + """ + attributes = {} + + # Extract user/agent/run IDs + for id_type in ["user_id", "agent_id", "run_id"]: + if id_type in kwargs and kwargs[id_type]: + # Use the new mem0-specific attributes + if id_type == "user_id": + attributes["mem0.user_id"] = str(kwargs[id_type]) + elif id_type == "agent_id": + attributes["mem0.agent_id"] = str(kwargs[id_type]) + elif id_type == "run_id": + attributes["mem0.run_id"] = str(kwargs[id_type]) + + # Extract metadata + if "metadata" in kwargs: + metadata = kwargs["metadata"] + if isinstance(metadata, dict): + for key, value in metadata.items(): + attributes[f"mem0.metadata.{key}"] = str(value) + + return attributes + + +def _extract_memory_response_attributes(return_value: Any) -> AttributeMap: + """Extract attributes from memory operation response. + + Args: + return_value: The response from the memory operation + + Returns: + Dictionary of extracted response attributes + """ + attributes = {} + + if return_value: + if isinstance(return_value, dict): + # Check if this is an update/delete response (simple message format) + if "message" in return_value and len(return_value) == 1: + # Handle update/delete operation response + attributes["mem0.operation.message"] = return_value["message"] + return attributes + + # Check if this is a single memory object (like from get method) + if "id" in return_value and "memory" in return_value and "results" not in return_value: + # Handle single memory object + attributes["mem0.memory_id"] = return_value["id"] + attributes["mem0.memory.0.id"] = return_value["id"] + attributes["mem0.memory.0.content"] = return_value["memory"] + attributes["mem0.results_count"] = 1 + + # Extract hash + if "hash" in return_value: + attributes["mem0.memory.0.hash"] = return_value["hash"] + + # Extract score (might be None for get operations) + if "score" in return_value and return_value["score"] is not None: + attributes["mem0.memory.0.score"] = str(return_value["score"]) + + # Extract metadata + if "metadata" in return_value and isinstance(return_value["metadata"], dict): + for key, value in return_value["metadata"].items(): + attributes[f"mem0.memory.0.metadata.{key}"] = str(value) + + # Extract timestamps + if "created_at" in return_value: + attributes["mem0.memory.0.created_at"] = return_value["created_at"] + + if "updated_at" in return_value and return_value["updated_at"]: + attributes["mem0.memory.0.updated_at"] = return_value["updated_at"] + + # Extract user_id + if "user_id" in return_value: + attributes["mem0.memory.0.user_id"] = return_value["user_id"] + attributes["mem0.user_ids"] = return_value["user_id"] + + return attributes + + # Extract status if present + if "status" in return_value: + attributes["mem0.status"] = str(return_value["status"]) + + # Extract results array - this is the main structure from mem0 (add/search operations) + if "results" in return_value and isinstance(return_value["results"], list): + results = return_value["results"] + attributes["mem0.results_count"] = len(results) + + # Extract event types + event_types = set() + memory_ids = [] + memory_contents = [] + scores = [] + user_ids = set() + + for i, result in enumerate(results): + if isinstance(result, dict): + # Extract event type + if "event" in result: + event_types.add(result["event"]) + + # Extract memory ID + if "id" in result: + memory_ids.append(result["id"]) + # Set individual memory ID attributes + attributes[f"mem0.memory.{i}.id"] = result["id"] + + # Extract memory content + if "memory" in result: + memory_contents.append(result["memory"]) + # Set individual memory content attributes + attributes[f"mem0.memory.{i}.content"] = result["memory"] + + # Extract event for individual result + if "event" in result: + attributes[f"mem0.memory.{i}.event"] = result["event"] + + # Extract hash + if "hash" in result: + attributes[f"mem0.memory.{i}.hash"] = result["hash"] + + # Extract score (for search results) + if "score" in result: + scores.append(result["score"]) + attributes[f"mem0.memory.{i}.score"] = str(result["score"]) + + # Extract metadata + if "metadata" in result and isinstance(result["metadata"], dict): + for key, value in result["metadata"].items(): + attributes[f"mem0.memory.{i}.metadata.{key}"] = str(value) + + # Extract timestamps + if "created_at" in result: + attributes[f"mem0.memory.{i}.created_at"] = result["created_at"] + + if "updated_at" in result and result["updated_at"]: + attributes[f"mem0.memory.{i}.updated_at"] = result["updated_at"] + + # Extract user_id + if "user_id" in result: + user_ids.add(result["user_id"]) + attributes[f"mem0.memory.{i}.user_id"] = result["user_id"] + + # Set aggregated attributes + if event_types: + attributes["mem0.event_types"] = ",".join(event_types) + + if memory_ids: + # Set primary memory ID (first one) as the main memory ID + attributes["mem0.memory_id"] = memory_ids[0] + # Set all memory IDs as a comma-separated list + attributes["mem0.memory.ids"] = ",".join(memory_ids) + + if memory_contents: + # Set all memory contents as a combined attribute + attributes["mem0.memory.contents"] = " | ".join(memory_contents) + + if scores: + # Set average and max scores for search results + attributes["mem0.search.avg_score"] = str(sum(scores) / len(scores)) + attributes["mem0.search.max_score"] = str(max(scores)) + attributes["mem0.search.min_score"] = str(min(scores)) + + if user_ids: + # Set user IDs (typically should be the same user for all results) + attributes["mem0.user_ids"] = ",".join(user_ids) + + # Extract relations count if present (for backward compatibility) + if "relations" in return_value: + attributes["mem0.relations_count"] = len(return_value["relations"]) + + elif isinstance(return_value, list): + # For operations that return lists directly (like search, get_all) + attributes["mem0.results_count"] = len(return_value) + + # If it's a list of memory objects, extract similar attributes + for i, item in enumerate(return_value): + if isinstance(item, dict): + if "id" in item: + attributes[f"mem0.memory.{i}.id"] = item["id"] + if "memory" in item: + attributes[f"mem0.memory.{i}.content"] = item["memory"] + if "event" in item: + attributes[f"mem0.memory.{i}.event"] = item["event"] + if "hash" in item: + attributes[f"mem0.memory.{i}.hash"] = item["hash"] + if "score" in item: + attributes[f"mem0.memory.{i}.score"] = str(item["score"]) + if "user_id" in item: + attributes[f"mem0.memory.{i}.user_id"] = item["user_id"] + + return attributes + + +def create_mem0_wrapper(operation_name: str, attribute_extractor): + """Create a wrapper function for Mem0 operations that ensures proper span hierarchy. + + This function creates wrappers that explicitly use the current context to ensure + mem0 spans are properly nested within the current AgentOps session or OpenAI spans. + + Args: + operation_name: Name of the mem0 operation (add, search, etc.) + attribute_extractor: Function to extract attributes for this operation + + Returns: + A wrapper function that creates properly nested spans + """ + + def wrapper(tracer): + def actual_wrapper(wrapped, instance, args, kwargs): + # Skip instrumentation if suppressed + from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY + + if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY): + return wrapped(*args, **kwargs) + + # Get current context to ensure proper parent-child relationship + current_context = context_api.get_current() + span = tracer.start_span( + f"mem0.memory.{operation_name}", + context=current_context, + kind=SpanKind.CLIENT, + attributes={SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value}, + ) + + return_value = None + try: + # Add the input attributes to the span before execution + attributes = attribute_extractor(args=args, kwargs=kwargs) + for key, value in attributes.items(): + span.set_attribute(key, value) + + return_value = wrapped(*args, **kwargs) + # Add the output attributes to the span after execution + attributes = attribute_extractor(return_value=return_value) + for key, value in attributes.items(): + span.set_attribute(key, value) + + span.set_status(Status(StatusCode.OK)) + except Exception as e: + # Add everything we have in the case of an error + attributes = attribute_extractor(args=args, kwargs=kwargs, return_value=return_value) + for key, value in attributes.items(): + span.set_attribute(key, value) + + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) + raise + finally: + span.end() + + return return_value + + return actual_wrapper + + return wrapper + + +def create_async_mem0_wrapper(operation_name: str, attribute_extractor): + """Create an async wrapper function for Mem0 operations that ensures proper span hierarchy. + + This function creates async wrappers that explicitly use the current context to ensure + mem0 spans are properly nested within the current AgentOps session or OpenAI spans. + + Args: + operation_name: Name of the mem0 operation (add, search, etc.) + attribute_extractor: Function to extract attributes for this operation + + Returns: + An async wrapper function that creates properly nested spans + """ + + def wrapper(tracer): + def actual_wrapper(wrapped, instance, args, kwargs): + # Skip instrumentation if suppressed + from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY + + if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY): + return wrapped(*args, **kwargs) + + async def async_wrapper(): + # Get current context to ensure proper parent-child relationship + current_context = context_api.get_current() + span = tracer.start_span( + f"mem0.AsyncMemory.{operation_name}", + context=current_context, + kind=SpanKind.CLIENT, + attributes={SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value}, + ) + + return_value = None + try: + # Add the input attributes to the span before execution + attributes = attribute_extractor(args=args, kwargs=kwargs) + for key, value in attributes.items(): + span.set_attribute(key, value) + + return_value = await wrapped(*args, **kwargs) + + # Add the output attributes to the span after execution + attributes = attribute_extractor(return_value=return_value) + for key, value in attributes.items(): + span.set_attribute(key, value) + + span.set_status(Status(StatusCode.OK)) + except Exception as e: + # Add everything we have in the case of an error + attributes = attribute_extractor(args=args, kwargs=kwargs, return_value=return_value) + for key, value in attributes.items(): + span.set_attribute(key, value) + + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) + raise + finally: + span.end() + + return return_value + + return async_wrapper() + + return actual_wrapper + + return wrapper + + +def create_universal_mem0_wrapper(operation_name: str, attribute_extractor): + """Create a universal wrapper that handles both sync and async methods. + + This function detects whether the wrapped method is async and applies the appropriate wrapper. + """ + + def wrapper(tracer): + def actual_wrapper(wrapped, instance, args, kwargs): + import asyncio + + # Check if the wrapped function is async + if asyncio.iscoroutinefunction(wrapped): + # Use async wrapper + async_wrapper_func = create_async_mem0_wrapper(operation_name, attribute_extractor) + return async_wrapper_func(tracer)(wrapped, instance, args, kwargs) + else: + # Use sync wrapper + sync_wrapper_func = create_mem0_wrapper(operation_name, attribute_extractor) + return sync_wrapper_func(tracer)(wrapped, instance, args, kwargs) + + return actual_wrapper + + return wrapper diff --git a/agentops/instrumentation/mem0/instrumentor.py b/agentops/instrumentation/mem0/instrumentor.py new file mode 100644 index 000000000..5f40515bf --- /dev/null +++ b/agentops/instrumentation/mem0/instrumentor.py @@ -0,0 +1,283 @@ +from typing import Collection +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.trace import get_tracer +from opentelemetry.metrics import get_meter +from wrapt import wrap_function_wrapper + +from agentops.instrumentation.mem0 import LIBRARY_NAME, LIBRARY_VERSION +from agentops.logging import logger + +# Import from refactored structure +from .memory import ( + mem0_add_wrapper, + mem0_search_wrapper, + mem0_get_all_wrapper, + mem0_delete_wrapper, + mem0_update_wrapper, + mem0_get_wrapper, + mem0_delete_all_wrapper, + mem0_history_wrapper, +) + +from agentops.semconv import Meters + +# Configure logging +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + +# Methods to wrap for instrumentation using specialized wrappers +WRAPPER_METHODS = [ + # Sync Memory class methods + { + "package": "mem0.memory.main", + "class_method": "Memory.add", + "wrapper": mem0_add_wrapper, + }, + { + "package": "mem0.memory.main", + "class_method": "Memory.search", + "wrapper": mem0_search_wrapper, + }, + { + "package": "mem0.memory.main", + "class_method": "Memory.get_all", + "wrapper": mem0_get_all_wrapper, + }, + { + "package": "mem0.memory.main", + "class_method": "Memory.get", + "wrapper": mem0_get_wrapper, + }, + { + "package": "mem0.memory.main", + "class_method": "Memory.delete", + "wrapper": mem0_delete_wrapper, + }, + { + "package": "mem0.memory.main", + "class_method": "Memory.delete_all", + "wrapper": mem0_delete_all_wrapper, + }, + { + "package": "mem0.memory.main", + "class_method": "Memory.update", + "wrapper": mem0_update_wrapper, + }, + { + "package": "mem0.memory.main", + "class_method": "Memory.history", + "wrapper": mem0_history_wrapper, + }, + # MemoryClient class methods + { + "package": "mem0.client.main", + "class_method": "MemoryClient.add", + "wrapper": mem0_add_wrapper, + }, + { + "package": "mem0.client.main", + "class_method": "MemoryClient.search", + "wrapper": mem0_search_wrapper, + }, + { + "package": "mem0.client.main", + "class_method": "MemoryClient.get_all", + "wrapper": mem0_get_all_wrapper, + }, + { + "package": "mem0.client.main", + "class_method": "MemoryClient.get", + "wrapper": mem0_get_wrapper, + }, + { + "package": "mem0.client.main", + "class_method": "MemoryClient.delete", + "wrapper": mem0_delete_wrapper, + }, + { + "package": "mem0.client.main", + "class_method": "MemoryClient.delete_all", + "wrapper": mem0_delete_all_wrapper, + }, + { + "package": "mem0.client.main", + "class_method": "MemoryClient.update", + "wrapper": mem0_update_wrapper, + }, + # AsyncMemoryClient class methods + { + "package": "mem0.client.main", + "class_method": "AsyncMemoryClient.add", + "wrapper": mem0_add_wrapper, + }, + { + "package": "mem0.client.main", + "class_method": "AsyncMemoryClient.search", + "wrapper": mem0_search_wrapper, + }, + { + "package": "mem0.client.main", + "class_method": "AsyncMemoryClient.get_all", + "wrapper": mem0_get_all_wrapper, + }, + { + "package": "mem0.client.main", + "class_method": "AsyncMemoryClient.get", + "wrapper": mem0_get_wrapper, + }, + { + "package": "mem0.client.main", + "class_method": "AsyncMemoryClient.delete", + "wrapper": mem0_delete_wrapper, + }, + { + "package": "mem0.client.main", + "class_method": "AsyncMemoryClient.delete_all", + "wrapper": mem0_delete_all_wrapper, + }, + { + "package": "mem0.client.main", + "class_method": "AsyncMemoryClient.update", + "wrapper": mem0_update_wrapper, + }, + # AsyncMemory class methods + { + "package": "mem0.memory.main", + "class_method": "AsyncMemory.add", + "wrapper": mem0_add_wrapper, + }, + { + "package": "mem0.memory.main", + "class_method": "AsyncMemory.search", + "wrapper": mem0_search_wrapper, + }, + { + "package": "mem0.memory.main", + "class_method": "AsyncMemory.get_all", + "wrapper": mem0_get_all_wrapper, + }, + { + "package": "mem0.memory.main", + "class_method": "AsyncMemory.get", + "wrapper": mem0_get_wrapper, + }, + { + "package": "mem0.memory.main", + "class_method": "AsyncMemory.delete", + "wrapper": mem0_delete_wrapper, + }, + { + "package": "mem0.memory.main", + "class_method": "AsyncMemory.delete_all", + "wrapper": mem0_delete_all_wrapper, + }, + { + "package": "mem0.memory.main", + "class_method": "AsyncMemory.update", + "wrapper": mem0_update_wrapper, + }, + { + "package": "mem0.memory.main", + "class_method": "AsyncMemory.history", + "wrapper": mem0_history_wrapper, + }, +] + + +class Mem0Instrumentor(BaseInstrumentor): + """An instrumentor for Mem0's client library. + + This class provides instrumentation for Mem0's memory operations by wrapping key methods + in the Memory, AsyncMemory, MemoryClient, and AsyncMemoryClient classes. It captures + telemetry data for memory operations including add, search, get, delete, delete_all, + update, and history operations. + + The instrumentor gracefully handles missing optional dependencies - if a provider's + package is not installed, it will be skipped without causing errors. + + It captures metrics including operation duration, memory counts, and exceptions. + """ + + def instrumentation_dependencies(self) -> Collection[str]: + """Return packages required for instrumentation. + + Returns: + A collection of package specifications required for this instrumentation. + """ + return ["mem0ai >= 0.1.10"] + + def _instrument(self, **kwargs): + """Instrument the Mem0 Memory API. + + This method wraps the key methods in the Mem0 Memory client to capture + telemetry data for memory operations. It sets up tracers, meters, and wraps the + appropriate methods for instrumentation. + + Args: + **kwargs: Configuration options for instrumentation. + """ + super()._instrument(**kwargs) + logger.debug("Starting Mem0 instrumentation...") + + tracer_provider = kwargs.get("tracer_provider") + tracer = get_tracer(LIBRARY_NAME, LIBRARY_VERSION, tracer_provider) + + meter_provider = kwargs.get("meter_provider") + meter = get_meter(LIBRARY_NAME, LIBRARY_VERSION, meter_provider) + + # Create metrics for memory operations + meter.create_histogram( + name=Meters.LLM_OPERATION_DURATION, + unit="s", + description="Mem0 memory operation duration", + ) + + meter.create_counter( + name=Meters.LLM_COMPLETIONS_EXCEPTIONS, + unit="time", + description="Number of exceptions occurred during Mem0 operations", + ) + + meter.create_histogram( + name="mem0.memory.count", + unit="memory", + description="Number of memories processed in Mem0 operations", + ) + + # Use specialized wrappers that ensure proper context hierarchy + for method_config in WRAPPER_METHODS: + try: + package = method_config["package"] + class_method = method_config["class_method"] + wrapper_func = method_config["wrapper"] + + logger.debug(f"Attempting to wrap {package}.{class_method} with {wrapper_func}") + wrap_function_wrapper(package, class_method, wrapper_func(tracer)) + logger.debug(f"Successfully wrapped {package}.{class_method}") + except (AttributeError, ModuleNotFoundError) as e: + # Use debug level for missing optional packages instead of error + # since LLM providers are optional dependencies + logger.debug(f"Skipping {package}.{class_method} - package not installed: {e}") + except Exception as e: + # Log unexpected errors as warnings + logger.warning(f"Unexpected error wrapping {package}.{class_method}: {e}") + + def _uninstrument(self, **kwargs): + """Remove instrumentation from Mem0 Memory API. + + This method unwraps all methods that were wrapped during instrumentation, + restoring the original behavior of the Mem0 Memory API. + + Args: + **kwargs: Configuration options for uninstrumentation. + """ + # Unwrap specialized methods + from opentelemetry.instrumentation.utils import unwrap + + for method_config in WRAPPER_METHODS: + try: + package = method_config["package"] + class_method = method_config["class_method"] + unwrap(package, class_method) + except Exception as e: + logger.debug(f"Failed to unwrap {package}.{class_method}: {e}") diff --git a/agentops/instrumentation/mem0/memory.py b/agentops/instrumentation/mem0/memory.py new file mode 100644 index 000000000..9220a15c7 --- /dev/null +++ b/agentops/instrumentation/mem0/memory.py @@ -0,0 +1,430 @@ +"""Memory operation attribute extractors and wrappers for Mem0 instrumentation.""" + +from typing import Optional, Tuple, Dict, Any + +from agentops.instrumentation.common.attributes import AttributeMap +from agentops.semconv import SpanAttributes, LLMRequestTypeValues, MessageAttributes +from .common import ( + get_common_mem0_attributes, + _extract_common_kwargs_attributes, + _extract_memory_response_attributes, + create_universal_mem0_wrapper, +) + + +def get_add_attributes( + args: Optional[Tuple] = None, kwargs: Optional[Dict[str, Any]] = None, return_value: Optional[Any] = None +) -> AttributeMap: + """Extract attributes for Mem0's add method. + + Args: + args: Positional arguments to the method + kwargs: Keyword arguments to the method + return_value: Return value from the method + + Returns: + Dictionary of extracted attributes + """ + print(f"args: {args}") + print(f"kwargs: {kwargs}") + print(f"return_value: {return_value}") + attributes = get_common_mem0_attributes() + attributes[SpanAttributes.OPERATION_NAME] = "add" + attributes[SpanAttributes.LLM_REQUEST_TYPE] = LLMRequestTypeValues.CHAT.value + + # Extract message content from args + if args and len(args) > 0: + messages = args[0] + # Get user_id from kwargs for speaker, default to "user" if not found + speaker = kwargs.get("user_id", "user") if kwargs else "user" + + if isinstance(messages, str): + attributes["mem0.message"] = messages + # Set as prompt for consistency with LLM patterns + attributes[MessageAttributes.PROMPT_CONTENT.format(i=0)] = messages + attributes[MessageAttributes.PROMPT_SPEAKER.format(i=0)] = speaker + elif isinstance(messages, list): + attributes["mem0.message_count"] = len(messages) + # Extract message types if available + message_types = set() + for i, msg in enumerate(messages): + if isinstance(msg, dict): + if "role" in msg: + message_types.add(msg["role"]) + attributes[MessageAttributes.PROMPT_ROLE.format(i=i)] = msg["role"] + if "content" in msg: + attributes[MessageAttributes.PROMPT_CONTENT.format(i=i)] = msg["content"] + # Set speaker for each message + attributes[MessageAttributes.PROMPT_SPEAKER.format(i=i)] = speaker + else: + # String message + attributes[MessageAttributes.PROMPT_CONTENT.format(i=i)] = str(msg) + + attributes[MessageAttributes.PROMPT_SPEAKER.format(i=i)] = speaker + if message_types: + attributes["mem0.message_types"] = ",".join(message_types) + + # Extract kwargs attributes + if kwargs: + attributes.update(_extract_common_kwargs_attributes(kwargs)) + + # Extract memory type + if "memory_type" in kwargs: + attributes["mem0.memory_type"] = str(kwargs["memory_type"]) + + # Extract inference flag + if "infer" in kwargs: + attributes[SpanAttributes.MEM0_INFER] = str(kwargs["infer"]) + + # Extract response attributes + if return_value: + attributes.update(_extract_memory_response_attributes(return_value)) + + return attributes + + +def get_search_attributes( + args: Optional[Tuple] = None, kwargs: Optional[Dict[str, Any]] = None, return_value: Optional[Any] = None +) -> AttributeMap: + """Extract attributes for Mem0's search method. + + Args: + args: Positional arguments to the method + kwargs: Keyword arguments to the method + return_value: Return value from the method + + Returns: + Dictionary of extracted attributes + """ + print(f"get_search_attributes args: {args}") + print(f"get_search_attributes kwargs: {kwargs}") + print(f"get_search_attributes return_value: {return_value}") + attributes = get_common_mem0_attributes() + attributes[SpanAttributes.OPERATION_NAME] = "search" + attributes[SpanAttributes.LLM_REQUEST_TYPE] = LLMRequestTypeValues.CHAT.value + + # Extract search query from args + if args and len(args) > 0: + query = args[0] + # Get user_id from kwargs for speaker, default to "user" if not found + speaker = kwargs.get("user_id", "user") if kwargs else "user" + + if isinstance(query, str): + attributes["mem0.message"] = query + # Set as prompt for consistency + attributes[MessageAttributes.PROMPT_CONTENT.format(i=0)] = query + attributes[MessageAttributes.PROMPT_SPEAKER.format(i=0)] = speaker + + # Extract kwargs attributes + if kwargs: + attributes.update(_extract_common_kwargs_attributes(kwargs)) + + # Extract memory type + if "memory_type" in kwargs: + attributes["mem0.memory_type"] = str(kwargs["memory_type"]) + + # Extract limit parameter + if "limit" in kwargs: + attributes["mem0.search.limit"] = str(kwargs["limit"]) + + # Extract response attributes + if return_value: + attributes.update(_extract_memory_response_attributes(return_value)) + + return attributes + + +def get_get_all_attributes( + args: Optional[Tuple] = None, kwargs: Optional[Dict[str, Any]] = None, return_value: Optional[Any] = None +) -> AttributeMap: + """Extract attributes for Mem0's get_all method. + + Args: + args: Positional arguments to the method + kwargs: Keyword arguments to the method + return_value: Return value from the method + + Returns: + Dictionary of extracted attributes + """ + attributes = get_common_mem0_attributes() + attributes[SpanAttributes.OPERATION_NAME] = "get_all" + + # Extract kwargs attributes + if kwargs: + attributes.update(_extract_common_kwargs_attributes(kwargs)) + + # Extract memory type + if "memory_type" in kwargs: + attributes["mem0.memory_type"] = str(kwargs["memory_type"]) + + # Extract response attributes + if return_value: + attributes.update(_extract_memory_response_attributes(return_value)) + + return attributes + + +def get_get_attributes( + args: Optional[Tuple] = None, kwargs: Optional[Dict[str, Any]] = None, return_value: Optional[Any] = None +) -> AttributeMap: + """Extract attributes for Mem0's get method. + + Args: + args: Positional arguments to the method + kwargs: Keyword arguments to the method + return_value: Return value from the method + + Returns: + Dictionary of extracted attributes + """ + attributes = get_common_mem0_attributes() + attributes[SpanAttributes.OPERATION_NAME] = "get" + + # Extract memory ID from args + if args and len(args) > 0: + memory_id = args[0] + if memory_id: + attributes["mem0.memory_id"] = str(memory_id) + + # Extract response attributes + if return_value: + attributes.update(_extract_memory_response_attributes(return_value)) + + return attributes + + +def get_delete_attributes( + args: Optional[Tuple] = None, kwargs: Optional[Dict[str, Any]] = None, return_value: Optional[Any] = None +) -> AttributeMap: + """Extract attributes for Mem0's delete method. + + Args: + args: Positional arguments to the method + kwargs: Keyword arguments to the method + return_value: Return value from the method + + Returns: + Dictionary of extracted attributes + """ + attributes = get_common_mem0_attributes() + attributes[SpanAttributes.OPERATION_NAME] = "delete" + + # Extract memory ID from args if available + if args and len(args) > 0: + memory_id = args[0] + if memory_id: + attributes["mem0.memory_id"] = str(memory_id) + + # Extract kwargs attributes if available + if kwargs: + attributes.update(_extract_common_kwargs_attributes(kwargs)) + + # Extract memory type + if "memory_type" in kwargs: + attributes["mem0.memory_type"] = str(kwargs["memory_type"]) + + # Extract response attributes + if return_value: + attributes.update(_extract_memory_response_attributes(return_value)) + + return attributes + + +def get_update_attributes( + args: Optional[Tuple] = None, kwargs: Optional[Dict[str, Any]] = None, return_value: Optional[Any] = None +) -> AttributeMap: + """Extract attributes for Mem0's update method. + + Args: + args: Positional arguments to the method + kwargs: Keyword arguments to the method + return_value: Return value from the method + + Returns: + Dictionary of extracted attributes + """ + attributes = get_common_mem0_attributes() + attributes[SpanAttributes.OPERATION_NAME] = "update" + + # Extract memory ID from args (if available) + if args and len(args) > 0: + memory_id = args[0] + if memory_id: + attributes["mem0.memory_id"] = str(memory_id) + + # Extract data from args (if available) + if args and len(args) > 1: + data = args[1] + if isinstance(data, str): + attributes["mem0.message"] = data + # Set as prompt for consistency + attributes[MessageAttributes.PROMPT_CONTENT.format(i=0)] = data + elif isinstance(data, dict): + # Handle case where data is a dictionary with "memory" key + if "memory" in data: + memory_text = data["memory"] + attributes["mem0.message"] = memory_text + # Set as prompt for consistency + attributes[MessageAttributes.PROMPT_CONTENT.format(i=0)] = memory_text + + # Extract metadata from data dict if present + if "metadata" in data and isinstance(data["metadata"], dict): + for key, value in data["metadata"].items(): + attributes[f"mem0.metadata.{key}"] = str(value) + + # Extract kwargs attributes (if available) + if kwargs: + attributes.update(_extract_common_kwargs_attributes(kwargs)) + + # Extract memory type + if "memory_type" in kwargs: + attributes["mem0.memory_type"] = str(kwargs["memory_type"]) + + # Extract response attributes + if return_value: + attributes.update(_extract_memory_response_attributes(return_value)) + + return attributes + + +def get_delete_all_attributes( + args: Optional[Tuple] = None, kwargs: Optional[Dict[str, Any]] = None, return_value: Optional[Any] = None +) -> AttributeMap: + """Extract attributes for Mem0's delete_all method. + + Args: + args: Positional arguments to the method + kwargs: Keyword arguments to the method + return_value: Return value from the method + + Returns: + Dictionary of extracted attributes + """ + attributes = get_common_mem0_attributes() + attributes[SpanAttributes.OPERATION_NAME] = "delete_all" + + # Extract kwargs attributes if available + if kwargs: + attributes.update(_extract_common_kwargs_attributes(kwargs)) + + # Extract memory type + if "memory_type" in kwargs: + attributes["mem0.memory_type"] = str(kwargs["memory_type"]) + + # Extract user_id for tracking which user's memories are being deleted + if "user_id" in kwargs: + attributes["mem0.delete_all.user_id"] = str(kwargs["user_id"]) + + # Extract agent_id for tracking which agent's memories are being deleted + if "agent_id" in kwargs: + attributes["mem0.delete_all.agent_id"] = str(kwargs["agent_id"]) + + # Extract run_id for tracking which run's memories are being deleted + if "run_id" in kwargs: + attributes["mem0.delete_all.run_id"] = str(kwargs["run_id"]) + + # Extract response attributes + if return_value: + attributes.update(_extract_memory_response_attributes(return_value)) + + return attributes + + +def get_history_attributes( + args: Optional[Tuple] = None, kwargs: Optional[Dict[str, Any]] = None, return_value: Optional[Any] = None +) -> AttributeMap: + """Extract attributes for Mem0's history method. + + Args: + args: Positional arguments to the method + kwargs: Keyword arguments to the method + return_value: Return value from the method + + Returns: + Dictionary of extracted attributes + """ + attributes = get_common_mem0_attributes() + attributes[SpanAttributes.OPERATION_NAME] = "history" + + # Extract memory ID from args + if args and len(args) > 0: + memory_id = args[0] + if memory_id: + attributes["mem0.memory_id"] = str(memory_id) + + # Extract kwargs attributes if available + if kwargs: + attributes.update(_extract_common_kwargs_attributes(kwargs)) + + # Extract history data from return value + if return_value and isinstance(return_value, list): + attributes["mem0.history.count"] = len(return_value) + + # Extract event types and other details from history entries + event_types = set() + actor_ids = set() + roles = set() + + for i, entry in enumerate(return_value): + if isinstance(entry, dict): + # Extract event type + if "event" in entry: + event_types.add(entry["event"]) + attributes[f"mem0.history.{i}.event"] = entry["event"] + + # Extract memory content changes + if "old_memory" in entry: + if entry["old_memory"]: + attributes[f"mem0.history.{i}.old_memory"] = entry["old_memory"] + + if "new_memory" in entry: + if entry["new_memory"]: + attributes[f"mem0.history.{i}.new_memory"] = entry["new_memory"] + + # Extract timestamps + if "created_at" in entry: + attributes[f"mem0.history.{i}.created_at"] = entry["created_at"] + + if "updated_at" in entry and entry["updated_at"]: + attributes[f"mem0.history.{i}.updated_at"] = entry["updated_at"] + + # Extract actor information + if "actor_id" in entry and entry["actor_id"]: + actor_ids.add(entry["actor_id"]) + attributes[f"mem0.history.{i}.actor_id"] = entry["actor_id"] + + if "role" in entry and entry["role"]: + roles.add(entry["role"]) + attributes[f"mem0.history.{i}.role"] = entry["role"] + + # Extract deletion status + if "is_deleted" in entry: + attributes[f"mem0.history.{i}.is_deleted"] = str(entry["is_deleted"]) + + # Extract history entry ID + if "id" in entry: + attributes[f"mem0.history.{i}.id"] = entry["id"] + + # Set aggregated attributes + if event_types: + attributes["mem0.history.event_types"] = ",".join(event_types) + + if actor_ids: + attributes["mem0.history.actor_ids"] = ",".join(actor_ids) + + if roles: + attributes["mem0.history.roles"] = ",".join(roles) + + return attributes + + +# Create universal Mem0 wrappers that work for both sync and async operations +mem0_add_wrapper = create_universal_mem0_wrapper("add", get_add_attributes) +mem0_search_wrapper = create_universal_mem0_wrapper("search", get_search_attributes) +mem0_get_all_wrapper = create_universal_mem0_wrapper("get_all", get_get_all_attributes) +mem0_get_wrapper = create_universal_mem0_wrapper("get", get_get_attributes) +mem0_delete_wrapper = create_universal_mem0_wrapper("delete", get_delete_attributes) +mem0_update_wrapper = create_universal_mem0_wrapper("update", get_update_attributes) +mem0_delete_all_wrapper = create_universal_mem0_wrapper("delete_all", get_delete_all_attributes) +mem0_history_wrapper = create_universal_mem0_wrapper("history", get_history_attributes) From 66a1ded421fed372ca0c690b4f9d49c01210e02c Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Sun, 1 Jun 2025 04:14:43 +0530 Subject: [PATCH 07/23] ruff checks --- agentops/instrumentation/mem0/instrumentor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/agentops/instrumentation/mem0/instrumentor.py b/agentops/instrumentation/mem0/instrumentor.py index 5f40515bf..403d47a0f 100644 --- a/agentops/instrumentation/mem0/instrumentor.py +++ b/agentops/instrumentation/mem0/instrumentor.py @@ -3,6 +3,7 @@ from opentelemetry.trace import get_tracer from opentelemetry.metrics import get_meter from wrapt import wrap_function_wrapper +import logging from agentops.instrumentation.mem0 import LIBRARY_NAME, LIBRARY_VERSION from agentops.logging import logger From 853eb54d1622868ee39570743fbd71005da15a51 Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Mon, 2 Jun 2025 20:10:15 +0530 Subject: [PATCH 08/23] why ruff again :( --- .../comprehensive_mem0_example.py | 201 ++++++++---------- 1 file changed, 85 insertions(+), 116 deletions(-) diff --git a/examples/mem0_examples/comprehensive_mem0_example.py b/examples/mem0_examples/comprehensive_mem0_example.py index 8c4fccacd..4aba1687d 100644 --- a/examples/mem0_examples/comprehensive_mem0_example.py +++ b/examples/mem0_examples/comprehensive_mem0_example.py @@ -13,7 +13,6 @@ import os import asyncio import logging -from typing import Dict, Any, List from dotenv import load_dotenv # Load environment variables @@ -21,13 +20,13 @@ # CRITICAL: Initialize AgentOps BEFORE importing mem0 classes # This ensures proper instrumentation and context propagation -import agentops +import agentops # noqa: E402 # Initialize AgentOps FIRST agentops.init(os.getenv("AGENTOPS_API_KEY")) # Now import mem0 classes AFTER agentops initialization -from mem0 import Memory, AsyncMemory, MemoryClient, AsyncMemoryClient +from mem0 import Memory, AsyncMemory, MemoryClient, AsyncMemoryClient # noqa: E402 # Set up logging logging.basicConfig(level=logging.INFO) @@ -36,13 +35,13 @@ # Configuration for local memory (Memory and AsyncMemory) local_config = { "llm": { - "provider": "openai", + "provider": "openai", "config": { "model": "gpt-4o-mini", "temperature": 0.1, "max_tokens": 2000, "api_key": os.getenv("OPENAI_API_KEY"), - } + }, } } @@ -59,57 +58,54 @@ {"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"}, {"role": "assistant", "content": "How about a thriller? They can be quite engaging."}, {"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."}, - {"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."} + { + "role": "assistant", + "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future.", + }, ] sample_preferences = [ "I prefer dark roast coffee over light roast", "I exercise every morning at 6 AM", - "I'm vegetarian and avoid all meat products", + "I'm vegetarian and avoid all meat products", "I love reading science fiction novels", - "I work in software engineering" + "I work in software engineering", ] def demonstrate_sync_memory(): """Demonstrate sync Memory class operations.""" - print("\n" + "="*60) + print("\n" + "=" * 60) print("๐Ÿง  SYNC MEMORY (Local) OPERATIONS") - print("="*60) - + print("=" * 60) + try: # Initialize sync Memory memory = Memory.from_config(local_config) print("โœ… Sync Memory initialized successfully") - + # 1. ADD operations print("\n๐Ÿ“ Adding memories...") - + # Add conversation messages result = memory.add( - sample_messages, - user_id=user_id, - metadata={"category": "movie_preferences", "session": "demo"} + sample_messages, user_id=user_id, metadata={"category": "movie_preferences", "session": "demo"} ) print(f" ๐Ÿ“Œ Added conversation: {result}") - + # Add individual preferences for i, preference in enumerate(sample_preferences): - result = memory.add( - preference, - user_id=user_id, - metadata={"type": "preference", "index": i} - ) + result = memory.add(preference, user_id=user_id, metadata={"type": "preference", "index": i}) print(f" ๐Ÿ“Œ Added preference {i+1}: {result}") - + # 2. SEARCH operations print("\n๐Ÿ” Searching memories...") search_queries = [ "What movies does the user like?", - "What are the user's food preferences?", + "What are the user's food preferences?", "When does the user exercise?", ] - + for query in search_queries: results = memory.search(query, user_id=user_id) print(f" ๐Ÿ”Ž Query: '{query}'") @@ -118,7 +114,7 @@ def demonstrate_sync_memory(): print(f" ๐Ÿ’ก Result {j+1}: {result.get('memory', 'N/A')}") else: print(" โŒ No results found") - + # 3. GET_ALL operations print("\n๐Ÿ“‹ Getting all memories...") all_memories = memory.get_all(user_id=user_id) @@ -126,12 +122,12 @@ def demonstrate_sync_memory(): print(f" ๐Ÿ“Š Total memories: {len(all_memories['results'])}") for i, mem in enumerate(all_memories["results"][:3]): # Show first 3 print(f" {i+1}. ID: {mem.get('id', 'N/A')[:8]}... | {mem.get('memory', 'N/A')[:50]}...") - + # Cleanup print("\n๐Ÿงน Cleaning up all memories...") delete_all_result = memory.delete_all(user_id=user_id) print(f" โœ… Delete all result: {delete_all_result}") - + except Exception as e: print(f"โŒ Sync Memory error: {e}") logger.error(f"Sync Memory demonstration failed: {e}") @@ -139,53 +135,49 @@ def demonstrate_sync_memory(): async def demonstrate_async_memory(): """Demonstrate async Memory class operations.""" - print("\n" + "="*60) + print("\n" + "=" * 60) print("๐Ÿš€ ASYNC MEMORY (Local) OPERATIONS") - print("="*60) - + print("=" * 60) + try: # Initialize async Memory async_memory = AsyncMemory.from_config(local_config) print("โœ… Async Memory initialized successfully") - + # 1. ADD operations print("\n๐Ÿ“ Adding memories asynchronously...") - + # Add conversation messages result = await async_memory.add( - sample_messages, - user_id=user_id, - metadata={"category": "async_movie_preferences", "session": "async_demo"} + sample_messages, user_id=user_id, metadata={"category": "async_movie_preferences", "session": "async_demo"} ) print(f" ๐Ÿ“Œ Added conversation: {result}") - + # Add preferences concurrently async def add_preference(preference, index): return await async_memory.add( - preference, - user_id=user_id, - metadata={"type": "async_preference", "index": index} + preference, user_id=user_id, metadata={"type": "async_preference", "index": index} ) - + tasks = [add_preference(pref, i) for i, pref in enumerate(sample_preferences)] results = await asyncio.gather(*tasks) for i, result in enumerate(results): print(f" ๐Ÿ“Œ Added async preference {i+1}: {result}") - + # 2. SEARCH operations print("\n๐Ÿ” Searching memories asynchronously...") search_queries = [ "What movies does the user like?", "What are the user's dietary restrictions?", - "What does the user do for work?" + "What does the user do for work?", ] - + async def search_memory(query): return await async_memory.search(query, user_id=user_id), query - + search_tasks = [search_memory(query) for query in search_queries] search_results = await asyncio.gather(*search_tasks) - + for result, query in search_results: print(f" ๐Ÿ”Ž Query: '{query}'") if result and "results" in result: @@ -193,7 +185,7 @@ async def search_memory(query): print(f" ๐Ÿ’ก Result {j+1}: {res.get('memory', 'N/A')}") else: print(" โŒ No results found") - + # 3. GET_ALL operations print("\n๐Ÿ“‹ Getting all memories asynchronously...") all_memories = await async_memory.get_all(user_id=user_id) @@ -201,12 +193,12 @@ async def search_memory(query): print(f" ๐Ÿ“Š Total async memories: {len(all_memories['results'])}") for i, mem in enumerate(all_memories["results"][:3]): print(f" {i+1}. ID: {mem.get('id', 'N/A')[:8]}... | {mem.get('memory', 'N/A')[:50]}...") - + # Cleanup print("\n๐Ÿงน Cleaning up all async memories...") delete_all_result = await async_memory.delete_all(user_id=user_id) print(f" โœ… Delete all result: {delete_all_result}") - + except Exception as e: print(f"โŒ Async Memory error: {e}") logger.error(f"Async Memory demonstration failed: {e}") @@ -214,62 +206,49 @@ async def search_memory(query): def demonstrate_sync_memory_client(): """Demonstrate sync MemoryClient class operations.""" - print("\n" + "="*60) + print("\n" + "=" * 60) print("โ˜๏ธ SYNC MEMORY CLIENT (Cloud) OPERATIONS") - print("="*60) - + print("=" * 60) + if not mem0_api_key: print("โŒ MEM0_API_KEY not found. Skipping cloud client operations.") return - + try: # Initialize sync MemoryClient client = MemoryClient(api_key=mem0_api_key) print("โœ… Sync MemoryClient initialized successfully") - + # 1. ADD operations print("\n๐Ÿ“ Adding memories to cloud...") - + # Add conversation result = client.add( - sample_messages, - user_id=user_id, - metadata={"category": "cloud_movie_preferences", "session": "cloud_demo"} + sample_messages, user_id=user_id, metadata={"category": "cloud_movie_preferences", "session": "cloud_demo"} ) print(f" ๐Ÿ“Œ Added conversation to cloud: {result}") - + # Add preferences for i, preference in enumerate(sample_preferences[:3]): # Limit for demo - result = client.add( - preference, - user_id=user_id, - metadata={"type": "cloud_preference", "index": i} - ) + result = client.add(preference, user_id=user_id, metadata={"type": "cloud_preference", "index": i}) print(f" ๐Ÿ“Œ Added cloud preference {i+1}: {result}") - + # 2. SEARCH operations print("\n๐Ÿ” Searching cloud memories...") - search_result = client.search( - "What are the user's movie preferences?", - user_id=user_id - ) + search_result = client.search("What are the user's movie preferences?", user_id=user_id) print(f" ๐Ÿ”Ž Search result: {search_result}") - + # 3. GET_ALL with filters print("\n๐Ÿ“‹ Getting all cloud memories with filters...") - filters = { - "AND": [ - {"user_id": user_id} - ] - } + filters = {"AND": [{"user_id": user_id}]} all_memories = client.get_all(filters=filters, limit=10) print(f" ๐Ÿ“Š Cloud memories retrieved: {all_memories}") - + # Cleanup print("\n๐Ÿงน Cleaning up cloud memories...") delete_all_result = client.delete_all(user_id=user_id) print(f" โœ… Delete all result: {delete_all_result}") - + except Exception as e: print(f"โŒ Sync MemoryClient error: {e}") logger.error(f"Sync MemoryClient demonstration failed: {e}") @@ -277,70 +256,60 @@ def demonstrate_sync_memory_client(): async def demonstrate_async_memory_client(): """Demonstrate async MemoryClient class operations.""" - print("\n" + "="*60) + print("\n" + "=" * 60) print("๐ŸŒ ASYNC MEMORY CLIENT (Cloud) OPERATIONS") - print("="*60) - + print("=" * 60) + if not mem0_api_key: print("โŒ MEM0_API_KEY not found. Skipping async cloud client operations.") return - + try: # Initialize async MemoryClient async_client = AsyncMemoryClient(api_key=mem0_api_key) print("โœ… Async MemoryClient initialized successfully") - + # 1. ADD operations concurrently print("\n๐Ÿ“ Adding memories to cloud asynchronously...") - + # Add conversation and preferences concurrently add_conversation_task = async_client.add( - sample_messages, - user_id=user_id, - metadata={"category": "async_cloud_movies", "session": "async_cloud_demo"} + sample_messages, user_id=user_id, metadata={"category": "async_cloud_movies", "session": "async_cloud_demo"} ) - + add_preference_tasks = [ - async_client.add( - pref, - user_id=user_id, - metadata={"type": "async_cloud_preference", "index": i} - ) + async_client.add(pref, user_id=user_id, metadata={"type": "async_cloud_preference", "index": i}) for i, pref in enumerate(sample_preferences[:3]) ] - + results = await asyncio.gather(add_conversation_task, *add_preference_tasks) print(f" ๐Ÿ“Œ Added conversation and preferences: {len(results)} items") for i, result in enumerate(results): print(f" {i+1}. {result}") - + # 2. Concurrent SEARCH operations print("\n๐Ÿ” Performing concurrent searches...") search_tasks = [ async_client.search("movie preferences", user_id=user_id), async_client.search("food preferences", user_id=user_id), - async_client.search("work information", user_id=user_id) + async_client.search("work information", user_id=user_id), ] - + search_results = await asyncio.gather(*search_tasks) for i, result in enumerate(search_results): print(f" ๐Ÿ”Ž Search {i+1} result: {result}") - + # 3. GET_ALL operation print("\n๐Ÿ“‹ Getting all async cloud memories...") - filters = { - "AND": [ - {"user_id": user_id} - ] - } + filters = {"AND": [{"user_id": user_id}]} all_memories = await async_client.get_all(filters=filters, limit=10) print(f" ๐Ÿ“Š Async cloud memories: {all_memories}") - + # Final cleanup print("\n๐Ÿงน Final cleanup of async cloud memories...") delete_all_result = await async_client.delete_all(user_id=user_id) print(f" โœ… Delete all result: {delete_all_result}") - + except Exception as e: print(f"โŒ Async MemoryClient error: {e}") logger.error(f"Async MemoryClient demonstration failed: {e}") @@ -350,18 +319,18 @@ def check_environment(): """Check if required environment variables are set.""" required_vars = ["AGENTOPS_API_KEY", "OPENAI_API_KEY"] optional_vars = ["MEM0_API_KEY"] - + missing_required = [var for var in required_vars if not os.getenv(var)] missing_optional = [var for var in optional_vars if not os.getenv(var)] - + if missing_required: print(f"โŒ Missing required environment variables: {missing_required}") return False - + if missing_optional: print(f"โš ๏ธ Missing optional environment variables: {missing_optional}") print(" Cloud client operations will be skipped.") - + return True @@ -373,27 +342,27 @@ async def main(): print("2. AsyncMemory (async local)") print("3. MemoryClient (sync cloud)") print("4. AsyncMemoryClient (async cloud)") - print("\n" + "="*80) - + print("\n" + "=" * 80) + if not check_environment(): print("Please set the required environment variables and try again.") return - + try: # Run all demonstrations demonstrate_sync_memory() await demonstrate_async_memory() demonstrate_sync_memory_client() await demonstrate_async_memory_client() - - print("\n" + "="*80) + + print("\n" + "=" * 80) print("โœ… COMPREHENSIVE MEM0 DEMONSTRATION COMPLETED!") print("Check your AgentOps dashboard to see the instrumentation data.") - + except Exception as e: logger.error(f"Demo failed: {e}") print(f"โŒ Demo failed: {e}") - + finally: # End AgentOps session agentops.end_session("Success") @@ -401,4 +370,4 @@ async def main(): if __name__ == "__main__": # Run the async main function - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) From aac9013a15fe3be224920087dd7c92db27e6033f Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Mon, 2 Jun 2025 20:13:57 +0530 Subject: [PATCH 09/23] ah! here we go again --- agentops/sdk/decorators/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/agentops/sdk/decorators/__init__.py b/agentops/sdk/decorators/__init__.py index f775b45d5..608b23908 100644 --- a/agentops/sdk/decorators/__init__.py +++ b/agentops/sdk/decorators/__init__.py @@ -20,6 +20,7 @@ tool = create_entity_decorator(SpanKind.TOOL) operation = task + # For backward compatibility: @session decorator calls @trace decorator @functools.wraps(trace) def session(*args, **kwargs): From e367492e91c91a5881139b451ecfd69d2f5ce5a1 Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Tue, 3 Jun 2025 04:42:10 +0530 Subject: [PATCH 10/23] updated the constants --- agentops/instrumentation/__init__.py | 145 +++++++++++++-------------- 1 file changed, 69 insertions(+), 76 deletions(-) diff --git a/agentops/instrumentation/__init__.py b/agentops/instrumentation/__init__.py index 6069957e2..c3619b3c6 100644 --- a/agentops/instrumentation/__init__.py +++ b/agentops/instrumentation/__init__.py @@ -32,6 +32,75 @@ from agentops.sdk.core import TracingCore +# Define the structure for instrumentor configurations +class InstrumentorConfig(TypedDict): + module_name: str + class_name: str + min_version: str + package_name: NotRequired[str] # Optional: actual pip package name if different from module + + +# Configuration for supported LLM providers +PROVIDERS: dict[str, InstrumentorConfig] = { + "openai": { + "module_name": "agentops.instrumentation.openai", + "class_name": "OpenAIInstrumentor", + "min_version": "1.0.0", + }, + "anthropic": { + "module_name": "agentops.instrumentation.anthropic", + "class_name": "AnthropicInstrumentor", + "min_version": "0.32.0", + }, + "ibm_watsonx_ai": { + "module_name": "agentops.instrumentation.ibm_watsonx_ai", + "class_name": "IBMWatsonXInstrumentor", + "min_version": "0.1.0", + }, + "google.genai": { + "module_name": "agentops.instrumentation.google_genai", + "class_name": "GoogleGenAIInstrumentor", + "min_version": "0.1.0", + "package_name": "google-genai", # Actual pip package name + }, +} + +# Configuration for utility instrumentors +UTILITY_INSTRUMENTORS: dict[str, InstrumentorConfig] = { + "concurrent.futures": { + "module_name": "agentops.instrumentation.concurrent_futures", + "class_name": "ConcurrentFuturesInstrumentor", + "min_version": "3.7.0", # Python 3.7+ (concurrent.futures is stdlib) + "package_name": "python", # Special case for stdlib modules + }, +} + +# Configuration for supported agentic libraries +AGENTIC_LIBRARIES: dict[str, InstrumentorConfig] = { + "crewai": { + "module_name": "agentops.instrumentation.crewai", + "class_name": "CrewAIInstrumentor", + "min_version": "0.56.0", + }, + "autogen": {"module_name": "agentops.instrumentation.ag2", "class_name": "AG2Instrumentor", "min_version": "0.1.0"}, + "agents": { + "module_name": "agentops.instrumentation.openai_agents", + "class_name": "OpenAIAgentsInstrumentor", + "min_version": "0.0.1", + }, + "google.adk": { + "module_name": "agentops.instrumentation.google_adk", + "class_name": "GoogleADKInstrumentor", + "min_version": "0.1.0", + }, +} + +# Combine all target packages for monitoring +TARGET_PACKAGES = set(PROVIDERS.keys()) | set(AGENTIC_LIBRARIES.keys()) | set(UTILITY_INSTRUMENTORS.keys()) + +# Create a single instance of the manager +# _manager = InstrumentationManager() # Removed + # Module-level state variables _active_instrumentors: list[BaseInstrumentor] = [] _original_builtins_import = builtins.__import__ # Store original import @@ -167,82 +236,6 @@ def _import_monitor(name: str, globals_dict=None, locals_dict=None, fromlist=(), return module -# Define the structure for instrumentor configurations -class InstrumentorConfig(TypedDict): - module_name: str - class_name: str - min_version: str - package_name: NotRequired[str] # Optional: actual pip package name if different from module - - -# Configuration for supported LLM providers -PROVIDERS: dict[str, InstrumentorConfig] = { - "openai": { - "module_name": "agentops.instrumentation.openai", - "class_name": "OpenAIInstrumentor", - "min_version": "1.0.0", - }, - "anthropic": { - "module_name": "agentops.instrumentation.anthropic", - "class_name": "AnthropicInstrumentor", - "min_version": "0.32.0", - }, - "ibm_watsonx_ai": { - "module_name": "agentops.instrumentation.ibm_watsonx_ai", - "class_name": "IBMWatsonXInstrumentor", - "min_version": "0.1.0", - }, - "google.genai": { - "module_name": "agentops.instrumentation.google_genai", - "class_name": "GoogleGenAIInstrumentor", - "min_version": "0.1.0", - "package_name": "google-genai", # Actual pip package name - }, - "mem0": { - "module_name": "agentops.instrumentation.mem0", - "class_name": "Mem0Instrumentor", - "min_version": "0.1.10", - "package_name": "mem0ai", # Actual pip package name - }, -} - -# Configuration for utility instrumentors -UTILITY_INSTRUMENTORS: dict[str, InstrumentorConfig] = { - "concurrent.futures": { - "module_name": "agentops.instrumentation.concurrent_futures", - "class_name": "ConcurrentFuturesInstrumentor", - "min_version": "3.7.0", # Python 3.7+ (concurrent.futures is stdlib) - "package_name": "python", # Special case for stdlib modules - }, -} - -# Configuration for supported agentic libraries -AGENTIC_LIBRARIES: dict[str, InstrumentorConfig] = { - "crewai": { - "module_name": "agentops.instrumentation.crewai", - "class_name": "CrewAIInstrumentor", - "min_version": "0.56.0", - }, - "autogen": {"module_name": "agentops.instrumentation.ag2", "class_name": "AG2Instrumentor", "min_version": "0.1.0"}, - "agents": { - "module_name": "agentops.instrumentation.openai_agents", - "class_name": "OpenAIAgentsInstrumentor", - "min_version": "0.0.1", - }, - "google.adk": { - "module_name": "agentops.instrumentation.google_adk", - "class_name": "GoogleADKInstrumentor", - "min_version": "0.1.0", - }, -} - -# Combine all target packages for monitoring -TARGET_PACKAGES = set(PROVIDERS.keys()) | set(AGENTIC_LIBRARIES.keys()) | set(UTILITY_INSTRUMENTORS.keys()) - -# Create a single instance of the manager -# _manager = InstrumentationManager() # Removed - - @dataclass class InstrumentorLoader: """ From 0f09692d3c414b5093f032636c6c0f6bb9b0da45 Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Tue, 3 Jun 2025 04:49:03 +0530 Subject: [PATCH 11/23] constants update --- agentops/instrumentation/__init__.py | 145 +++++++++++++-------------- 1 file changed, 69 insertions(+), 76 deletions(-) diff --git a/agentops/instrumentation/__init__.py b/agentops/instrumentation/__init__.py index 54e4b1c10..c3619b3c6 100644 --- a/agentops/instrumentation/__init__.py +++ b/agentops/instrumentation/__init__.py @@ -32,6 +32,75 @@ from agentops.sdk.core import TracingCore +# Define the structure for instrumentor configurations +class InstrumentorConfig(TypedDict): + module_name: str + class_name: str + min_version: str + package_name: NotRequired[str] # Optional: actual pip package name if different from module + + +# Configuration for supported LLM providers +PROVIDERS: dict[str, InstrumentorConfig] = { + "openai": { + "module_name": "agentops.instrumentation.openai", + "class_name": "OpenAIInstrumentor", + "min_version": "1.0.0", + }, + "anthropic": { + "module_name": "agentops.instrumentation.anthropic", + "class_name": "AnthropicInstrumentor", + "min_version": "0.32.0", + }, + "ibm_watsonx_ai": { + "module_name": "agentops.instrumentation.ibm_watsonx_ai", + "class_name": "IBMWatsonXInstrumentor", + "min_version": "0.1.0", + }, + "google.genai": { + "module_name": "agentops.instrumentation.google_genai", + "class_name": "GoogleGenAIInstrumentor", + "min_version": "0.1.0", + "package_name": "google-genai", # Actual pip package name + }, +} + +# Configuration for utility instrumentors +UTILITY_INSTRUMENTORS: dict[str, InstrumentorConfig] = { + "concurrent.futures": { + "module_name": "agentops.instrumentation.concurrent_futures", + "class_name": "ConcurrentFuturesInstrumentor", + "min_version": "3.7.0", # Python 3.7+ (concurrent.futures is stdlib) + "package_name": "python", # Special case for stdlib modules + }, +} + +# Configuration for supported agentic libraries +AGENTIC_LIBRARIES: dict[str, InstrumentorConfig] = { + "crewai": { + "module_name": "agentops.instrumentation.crewai", + "class_name": "CrewAIInstrumentor", + "min_version": "0.56.0", + }, + "autogen": {"module_name": "agentops.instrumentation.ag2", "class_name": "AG2Instrumentor", "min_version": "0.1.0"}, + "agents": { + "module_name": "agentops.instrumentation.openai_agents", + "class_name": "OpenAIAgentsInstrumentor", + "min_version": "0.0.1", + }, + "google.adk": { + "module_name": "agentops.instrumentation.google_adk", + "class_name": "GoogleADKInstrumentor", + "min_version": "0.1.0", + }, +} + +# Combine all target packages for monitoring +TARGET_PACKAGES = set(PROVIDERS.keys()) | set(AGENTIC_LIBRARIES.keys()) | set(UTILITY_INSTRUMENTORS.keys()) + +# Create a single instance of the manager +# _manager = InstrumentationManager() # Removed + # Module-level state variables _active_instrumentors: list[BaseInstrumentor] = [] _original_builtins_import = builtins.__import__ # Store original import @@ -167,82 +236,6 @@ def _import_monitor(name: str, globals_dict=None, locals_dict=None, fromlist=(), return module -# Define the structure for instrumentor configurations -class InstrumentorConfig(TypedDict): - module_name: str - class_name: str - min_version: str - package_name: NotRequired[str] # Optional: actual pip package name if different from module - - -# Configuration for supported LLM providers -PROVIDERS: dict[str, InstrumentorConfig] = { - "openai": { - "module_name": "agentops.instrumentation.openai", - "class_name": "OpenAIInstrumentor", - "min_version": "1.0.0", - }, - "anthropic": { - "module_name": "agentops.instrumentation.anthropic", - "class_name": "AnthropicInstrumentor", - "min_version": "0.32.0", - }, - "ibm_watsonx_ai": { - "module_name": "agentops.instrumentation.ibm_watsonx_ai", - "class_name": "IBMWatsonXInstrumentor", - "min_version": "0.1.0", - }, - "google.genai": { - "module_name": "agentops.instrumentation.google_genai", - "class_name": "GoogleGenAIInstrumentor", - "min_version": "0.1.0", - "package_name": "google-genai", # Actual pip package name - }, - # "mem0": { - # "module_name": "agentops.instrumentation.mem0", - # "class_name": "Mem0Instrumentor", - # "min_version": "0.1.10", - # "package_name": "mem0ai", # Actual pip package name - # }, -} - -# Configuration for utility instrumentors -UTILITY_INSTRUMENTORS: dict[str, InstrumentorConfig] = { - "concurrent.futures": { - "module_name": "agentops.instrumentation.concurrent_futures", - "class_name": "ConcurrentFuturesInstrumentor", - "min_version": "3.7.0", # Python 3.7+ (concurrent.futures is stdlib) - "package_name": "python", # Special case for stdlib modules - }, -} - -# Configuration for supported agentic libraries -AGENTIC_LIBRARIES: dict[str, InstrumentorConfig] = { - "crewai": { - "module_name": "agentops.instrumentation.crewai", - "class_name": "CrewAIInstrumentor", - "min_version": "0.56.0", - }, - "autogen": {"module_name": "agentops.instrumentation.ag2", "class_name": "AG2Instrumentor", "min_version": "0.1.0"}, - "agents": { - "module_name": "agentops.instrumentation.openai_agents", - "class_name": "OpenAIAgentsInstrumentor", - "min_version": "0.0.1", - }, - "google.adk": { - "module_name": "agentops.instrumentation.google_adk", - "class_name": "GoogleADKInstrumentor", - "min_version": "0.1.0", - }, -} - -# Combine all target packages for monitoring -TARGET_PACKAGES = set(PROVIDERS.keys()) | set(AGENTIC_LIBRARIES.keys()) | set(UTILITY_INSTRUMENTORS.keys()) - -# Create a single instance of the manager -# _manager = InstrumentationManager() # Removed - - @dataclass class InstrumentorLoader: """ From cd53e9d5af0a613792a089bce7f39bca08b63fb6 Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Sun, 8 Jun 2025 00:55:48 +0530 Subject: [PATCH 12/23] refactor the instrumentation with utlity instrumentation --- agentops/instrumentation/__init__.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/agentops/instrumentation/__init__.py b/agentops/instrumentation/__init__.py index e515cae94..a86189fea 100644 --- a/agentops/instrumentation/__init__.py +++ b/agentops/instrumentation/__init__.py @@ -119,6 +119,16 @@ def _is_installed_package(module_obj: ModuleType, package_name_key: str) -> bool rather than a local module, especially when names might collide. `package_name_key` is the key from TARGET_PACKAGES (e.g., 'agents', 'google.adk'). """ + # Special case for stdlib modules (marked with package_name="python" in UTILITY_INSTRUMENTORS) + if ( + package_name_key in UTILITY_INSTRUMENTORS + and UTILITY_INSTRUMENTORS[package_name_key].get("package_name") == "python" + ): + logger.debug( + f"_is_installed_package: Module '{package_name_key}' is a Python standard library module. Considering it an installed package." + ) + return True + if not hasattr(module_obj, "__file__") or not module_obj.__file__: logger.debug( f"_is_installed_package: Module '{package_name_key}' has no __file__, assuming it might be an SDK namespace package. Returning True." @@ -220,6 +230,12 @@ def _should_instrument_package(package_name: str) -> bool: logger.debug(f"_should_instrument_package: '{package_name}' already instrumented by AgentOps. Skipping.") return False + # Utility instrumentors should always be instrumented regardless of agentic library state + if package_name in UTILITY_INSTRUMENTORS: + logger.debug(f"_should_instrument_package: '{package_name}' is a utility instrumentor. Always allowing.") + return True + + # Only apply agentic/provider logic if it's NOT a utility instrumentor is_target_agentic = package_name in AGENTIC_LIBRARIES is_target_provider = package_name in PROVIDERS @@ -268,14 +284,18 @@ def _perform_instrumentation(package_name: str): return # Get the appropriate configuration for the package - # Ensure package_name is a key in either PROVIDERS or AGENTIC_LIBRARIES - if package_name not in PROVIDERS and package_name not in AGENTIC_LIBRARIES: + # Ensure package_name is a key in either PROVIDERS, AGENTIC_LIBRARIES, or UTILITY_INSTRUMENTORS + if ( + package_name not in PROVIDERS + and package_name not in AGENTIC_LIBRARIES + and package_name not in UTILITY_INSTRUMENTORS + ): logger.debug( - f"_perform_instrumentation: Package '{package_name}' not found in PROVIDERS or AGENTIC_LIBRARIES. Skipping." + f"_perform_instrumentation: Package '{package_name}' not found in PROVIDERS, AGENTIC_LIBRARIES, or UTILITY_INSTRUMENTORS. Skipping." ) return - config = PROVIDERS.get(package_name) or AGENTIC_LIBRARIES[package_name] + config = PROVIDERS.get(package_name) or AGENTIC_LIBRARIES.get(package_name) or UTILITY_INSTRUMENTORS[package_name] loader = InstrumentorLoader(**config) # instrument_one already checks loader.should_activate From c283fdd4fbeb3d4e4448ebf545e139f2a3fb02f1 Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Sun, 8 Jun 2025 02:48:16 +0530 Subject: [PATCH 13/23] code cleanup --- agentops/instrumentation/__init__.py | 6 +++ .../concurrent_futures/instrumentation.py | 39 +++++++++++++------ agentops/instrumentation/mem0/common.py | 2 +- agentops/instrumentation/mem0/instrumentor.py | 4 +- agentops/instrumentation/mem0/memory.py | 18 ++++----- 5 files changed, 45 insertions(+), 24 deletions(-) diff --git a/agentops/instrumentation/__init__.py b/agentops/instrumentation/__init__.py index e45995144..f9e1cce2f 100644 --- a/agentops/instrumentation/__init__.py +++ b/agentops/instrumentation/__init__.py @@ -67,6 +67,12 @@ class InstrumentorConfig(TypedDict): "min_version": "0.1.0", "package_name": "google-genai", # Actual pip package name }, + "mem0": { + "module_name": "agentops.instrumentation.mem0", + "class_name": "Mem0Instrumentor", + "min_version": "0.1.0", + "package_name": "mem0ai", # Actual pip package name + }, } # Configuration for utility instrumentors diff --git a/agentops/instrumentation/concurrent_futures/instrumentation.py b/agentops/instrumentation/concurrent_futures/instrumentation.py index 3ed4caee3..71c9b50f0 100644 --- a/agentops/instrumentation/concurrent_futures/instrumentation.py +++ b/agentops/instrumentation/concurrent_futures/instrumentation.py @@ -7,8 +7,9 @@ import contextvars import functools -from typing import Collection -from concurrent.futures import ThreadPoolExecutor +from typing import Any, Callable, Collection, Optional, Tuple, TypeVar + +from concurrent.futures import ThreadPoolExecutor, Future from opentelemetry.instrumentation.instrumentor import BaseInstrumentor @@ -18,16 +19,26 @@ _original_init = None _original_submit = None +# Type variables for better typing +T = TypeVar("T") +R = TypeVar("R") + -def _context_propagating_init(original_init): +def _context_propagating_init(original_init: Callable) -> Callable: """Wrap ThreadPoolExecutor.__init__ to set up context-aware initializer.""" @functools.wraps(original_init) - def wrapped_init(self, max_workers=None, thread_name_prefix="", initializer=None, initargs=()): + def wrapped_init( + self: ThreadPoolExecutor, + max_workers: Optional[int] = None, + thread_name_prefix: str = "", + initializer: Optional[Callable] = None, + initargs: Tuple = (), + ) -> None: # Capture the current context when the executor is created main_context = contextvars.copy_context() - def context_aware_initializer(): + def context_aware_initializer() -> None: """Initializer that sets up the captured context in each worker thread.""" logger.debug("[ConcurrentFuturesInstrumentor] Setting up context in worker thread") @@ -68,11 +79,11 @@ def context_aware_initializer(): return wrapped_init -def _context_propagating_submit(original_submit): +def _context_propagating_submit(original_submit: Callable) -> Callable: """Wrap ThreadPoolExecutor.submit to ensure context propagation.""" @functools.wraps(original_submit) - def wrapped_submit(self, func, *args, **kwargs): + def wrapped_submit(self: ThreadPoolExecutor, func: Callable[..., R], *args: Any, **kwargs: Any) -> Future[R]: # Log the submission func_name = getattr(func, "__name__", str(func)) logger.debug(f"[ConcurrentFuturesInstrumentor] Submitting function: {func_name}") @@ -97,7 +108,7 @@ def instrumentation_dependencies(self) -> Collection[str]: """Return a list of instrumentation dependencies.""" return [] - def _instrument(self, **kwargs): + def _instrument(self, **kwargs: Any) -> None: """Instrument the concurrent.futures module.""" global _original_init, _original_submit @@ -113,7 +124,7 @@ def _instrument(self, **kwargs): logger.info("[ConcurrentFuturesInstrumentor] Successfully instrumented concurrent.futures.ThreadPoolExecutor") - def _uninstrument(self, **kwargs): + def _uninstrument(self, **kwargs: Any) -> None: """Uninstrument the concurrent.futures module.""" global _original_init, _original_submit @@ -131,11 +142,14 @@ def _uninstrument(self, **kwargs): logger.info("[ConcurrentFuturesInstrumentor] Successfully uninstrumented concurrent.futures.ThreadPoolExecutor") @staticmethod - def instrument_module_directly(): + def instrument_module_directly() -> bool: """ Directly instrument the module without using the standard instrumentor interface. This can be called manually if automatic instrumentation is not desired. + + Returns: + bool: True if instrumentation was applied, False if already instrumented """ instrumentor = ConcurrentFuturesInstrumentor() if not instrumentor.is_instrumented_by_opentelemetry: @@ -144,11 +158,14 @@ def instrument_module_directly(): return False @staticmethod - def uninstrument_module_directly(): + def uninstrument_module_directly() -> bool: """ Directly uninstrument the module. This can be called manually to remove instrumentation. + + Returns: + bool: True if uninstrumentation was applied, False if already uninstrumented """ instrumentor = ConcurrentFuturesInstrumentor() if instrumentor.is_instrumented_by_opentelemetry: diff --git a/agentops/instrumentation/mem0/common.py b/agentops/instrumentation/mem0/common.py index 55576f78d..6fda783a5 100644 --- a/agentops/instrumentation/mem0/common.py +++ b/agentops/instrumentation/mem0/common.py @@ -8,7 +8,7 @@ from agentops.semconv import SpanAttributes, LLMRequestTypeValues -def get_common_mem0_attributes() -> AttributeMap: +def get_common_attributes() -> AttributeMap: """Get common instrumentation attributes for Mem0 operations. Returns: diff --git a/agentops/instrumentation/mem0/instrumentor.py b/agentops/instrumentation/mem0/instrumentor.py index 9f49c96ff..51a0dac60 100644 --- a/agentops/instrumentation/mem0/instrumentor.py +++ b/agentops/instrumentation/mem0/instrumentor.py @@ -246,10 +246,7 @@ def _instrument(self, **kwargs): package = method_config["package"] class_method = method_config["class_method"] wrapper_func = method_config["wrapper"] - - logger.debug(f"Attempting to wrap {package}.{class_method} with {wrapper_func}") wrap_function_wrapper(package, class_method, wrapper_func(tracer)) - logger.debug(f"Successfully wrapped {package}.{class_method}") except (AttributeError, ModuleNotFoundError) as e: # Use debug level for missing optional packages instead of error # since LLM providers are optional dependencies @@ -257,6 +254,7 @@ def _instrument(self, **kwargs): except Exception as e: # Log unexpected errors as warnings logger.warning(f"Unexpected error wrapping {package}.{class_method}: {e}") + logger.debug("Mem0 instrumentation completed") def _uninstrument(self, **kwargs): """Remove instrumentation from Mem0 Memory API. diff --git a/agentops/instrumentation/mem0/memory.py b/agentops/instrumentation/mem0/memory.py index 9220a15c7..04932c1c9 100644 --- a/agentops/instrumentation/mem0/memory.py +++ b/agentops/instrumentation/mem0/memory.py @@ -5,7 +5,7 @@ from agentops.instrumentation.common.attributes import AttributeMap from agentops.semconv import SpanAttributes, LLMRequestTypeValues, MessageAttributes from .common import ( - get_common_mem0_attributes, + get_common_attributes, _extract_common_kwargs_attributes, _extract_memory_response_attributes, create_universal_mem0_wrapper, @@ -28,7 +28,7 @@ def get_add_attributes( print(f"args: {args}") print(f"kwargs: {kwargs}") print(f"return_value: {return_value}") - attributes = get_common_mem0_attributes() + attributes = get_common_attributes() attributes[SpanAttributes.OPERATION_NAME] = "add" attributes[SpanAttributes.LLM_REQUEST_TYPE] = LLMRequestTypeValues.CHAT.value @@ -99,7 +99,7 @@ def get_search_attributes( print(f"get_search_attributes args: {args}") print(f"get_search_attributes kwargs: {kwargs}") print(f"get_search_attributes return_value: {return_value}") - attributes = get_common_mem0_attributes() + attributes = get_common_attributes() attributes[SpanAttributes.OPERATION_NAME] = "search" attributes[SpanAttributes.LLM_REQUEST_TYPE] = LLMRequestTypeValues.CHAT.value @@ -147,7 +147,7 @@ def get_get_all_attributes( Returns: Dictionary of extracted attributes """ - attributes = get_common_mem0_attributes() + attributes = get_common_attributes() attributes[SpanAttributes.OPERATION_NAME] = "get_all" # Extract kwargs attributes @@ -178,7 +178,7 @@ def get_get_attributes( Returns: Dictionary of extracted attributes """ - attributes = get_common_mem0_attributes() + attributes = get_common_attributes() attributes[SpanAttributes.OPERATION_NAME] = "get" # Extract memory ID from args @@ -207,7 +207,7 @@ def get_delete_attributes( Returns: Dictionary of extracted attributes """ - attributes = get_common_mem0_attributes() + attributes = get_common_attributes() attributes[SpanAttributes.OPERATION_NAME] = "delete" # Extract memory ID from args if available @@ -244,7 +244,7 @@ def get_update_attributes( Returns: Dictionary of extracted attributes """ - attributes = get_common_mem0_attributes() + attributes = get_common_attributes() attributes[SpanAttributes.OPERATION_NAME] = "update" # Extract memory ID from args (if available) @@ -301,7 +301,7 @@ def get_delete_all_attributes( Returns: Dictionary of extracted attributes """ - attributes = get_common_mem0_attributes() + attributes = get_common_attributes() attributes[SpanAttributes.OPERATION_NAME] = "delete_all" # Extract kwargs attributes if available @@ -344,7 +344,7 @@ def get_history_attributes( Returns: Dictionary of extracted attributes """ - attributes = get_common_mem0_attributes() + attributes = get_common_attributes() attributes[SpanAttributes.OPERATION_NAME] = "history" # Extract memory ID from args From ea8c1f09076772be2add8c0f10e5fa5009db274a Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Wed, 11 Jun 2025 04:52:30 +0530 Subject: [PATCH 14/23] added v2 docs --- docs/images/external/mem0/mem0.png | Bin 0 -> 3356 bytes docs/mint.json | 1 + docs/v2/examples/examples.mdx | 4 + docs/v2/examples/mem0.mdx | 474 ++++++++++++++++++ docs/v2/integrations/mem0.mdx | 256 ++++++++++ .../comprehensive_mem0_example.py | 6 +- 6 files changed, 738 insertions(+), 3 deletions(-) create mode 100644 docs/images/external/mem0/mem0.png create mode 100644 docs/v2/examples/mem0.mdx create mode 100644 docs/v2/integrations/mem0.mdx diff --git a/docs/images/external/mem0/mem0.png b/docs/images/external/mem0/mem0.png new file mode 100644 index 0000000000000000000000000000000000000000..c71241f42e23ee9efaade9331ae274f71d2d75f5 GIT binary patch literal 3356 zcmV+%4de2OP)u_vOfiAko@YNtNHH;;v(8lAs2I7(^CXA3FkqyjwM=s_ypSY3H8eo>z76t@`T!zxp?< zc{df{7X##XM9~NZq(~S+1uT>h1=Z%{&BE_Q+|@yJ0#N{YL;iyzQZXlG{Lc8y0O~^W z&$v)LlF!i!|MFtM)H#`(XM9wy$JDO&HUI@}(1gMLEE-(U0OM}zE5K4K9oiPRkR0*RPK_OGrz@gT(#ZVWC@rd?7;}#H_MH z6<3bvw|xGTO;mu8N)%LZ;k8Z_=3*Z811LDe7Xy8}pd!*YaQ54Krl%=Pf2dc5xZ*e1FX z4F#1zTrdKVf+$=8Q5^-KQ&Bn+6J|JS>Hwgxx7Rm?={7&ljH4GrBmBlrjL*AyZ%N9Q zrpDIj{`QQk?}mi=c0+*fNv+-qKk!0FTq8tRkT#;Kk~(SE?hijXdiXdJR$b$eXaJzC z^-)Q21rnIMIBD#}!2qyr>laOptpHG3RC8$WC6m>E%4L{Mf)@j3qM|Y)%06UhyqxKo*suZ5Mc39e-u&DSh-E{UuuF|Dw z$a*$in~bQ0wNT)VNqlI?NyrS@&=Et1jYvqDIoTc_Of0y0>v{nAW$N4$S3W*{{OX9< zWs`^hdU(`|@$qZJA_IMV`v5@P;K*gE$pCEC+-)mg`e65ar|_wAl9;SWRMV%KV=!Ej z>39d(1HU|%&iO;OBl zaKeU!`vE{mxIZ&_FLz7=0mwp`G^1#82&U$6IF24T3`m!*S{M*ytFNv9&6;;EXI&2o z3w-#n-P_j_i81-i1>+ImMo!`1_MHz5_F1=aPLSR9$&uG;DjFlB1I{iwfBj;!)xr=I?)*6)XkAV7oW$Rk-FNiy_DKK7rjdyQy19Kcafk@6AVpvV zc{BO)$XWzQKVWo}Pc(b9m*Raic21MKCZ5EP$c&X ziW_R{f*-O_}A~DHk;I$~yT^082}A$NFX4itbk4|FP=Lmv=vGd6Y1+zs0F+jq{bt#U^M5}n-(8aW^5*SnZ7nVPckHgOaV%Q> z^FhPn8SOf{?~9A4E+DexSrZnmnqjf9x@yPGoZCZ3J{uP4kD#O^tgo)DA2(_68}I!4 zkm1p4vY;l{GQ2mmhIwr*R8I?Z`+y>c0k-LvFJ~P)uHzq`IJ&&Jw6>}S0K9G9r}EAP zg$5uf1f0la=0s5PZ{{zbvc9#Y&BL?TXQ#IgA3b1H#PWwN?Ev5x;GJFkv5(DD05b9r zbI3^aIG##A$smo%ILPv=O&fzk?EnxzZ0PRH<0%UlNHhS@+R}O}=X+t`NqK2?OLIG8 zJbX$2G^4ep4FEbjx{mBSTlt_tq5*)WhSu`Zda*PSr=6&NNCfMCDqXCnp% zIBNEPu%ofAZtjXDqmsttT)dJxBTYc@B3}A-PTyw zkiI7q7+ta|IV8*`lS~A?M#IoJ_Go2B3XZM?0n~e$hYoIe-odR!Pn>{kTx~x1PB&fkrXR%n;{BB)CZS$_Q zow?U8PD_WaNNI|ZE7%`JqGjN3W>L4G9#1^z)cADh>_f16fb`~U#{wCTeyvp;iG zHGXyc3IKff^_^++#&PS$g~aL1R8hHXT~iqVGs+?wAT(4)di6@0Jy#Ytm}!y`NfZ2o zLl9}dLC+Cz=I7p!XaL}c8wHhRj;NThKkt3pQCX8bD^We&YH5NZBcOsa7yJ;j+q$~T zMj8mzWXTM!Bd~_*s?;fyD~j*i0{nMno`_2reDm71x~j@)3um`AH%xtY%%e7b$b&m_yJ!?ZAtEe66SXy)Bmhy-Wd@)gmtJBMQ#c=YmKEL&em*BkzXky-J_bym?`Vf>I~iM ze`fhGa4CU_La1H})vP|@Hil;iocII@$ZP?%al^2nD6CSGfi}ou1&tj~P9`-;xfwB> z06)>FqCwCS7Y(c6G9O2i-PO!A15>0&^uecAC)^-0oDoCQnG~YZ>TsyTLUmAjE>z`8 z(1cqI66@2;B<720dNoR42!_~ESD$=fRw&_a1&CJnWk968mRe)XL>H^RIebb<(iN8W zzE(Rx!&kd&f + } iconType="image" href="/v2/examples/mem0"> + Comprehensive memory operations with Mem0ai + + diff --git a/docs/v2/examples/mem0.mdx b/docs/v2/examples/mem0.mdx new file mode 100644 index 000000000..216d1a58e --- /dev/null +++ b/docs/v2/examples/mem0.mdx @@ -0,0 +1,474 @@ +--- +title: 'Mem0 Example' +description: 'Comprehensive Mem0 memory operations with AgentOps tracking' +--- + +_View Example on Github_ + +# AgentOps Mem0 Integration + +This comprehensive example demonstrates all four Mem0 memory classes with complete AgentOps instrumentation: +- **Memory** (Sync Local Memory) +- **AsyncMemory** (Async Local Memory) +- **MemoryClient** (Sync Cloud Client) +- **AsyncMemoryClient** (Async Cloud Client) + +## Installation + +Install the required packages: + + +```bash pip +pip install agentops mem0ai python-dotenv +``` +```bash poetry +poetry add agentops mem0ai python-dotenv +``` +```bash uv +uv add agentops mem0ai python-dotenv +``` + + +## Setup + +### Import Dependencies +Import the necessary libraries: + +```python +import os +import asyncio +import logging +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# This ensures proper instrumentation +import agentops + +# Initialize AgentOps +agentops.init(os.getenv("AGENTOPS_API_KEY")) + +# Now import mem0 +from mem0 import Memory, AsyncMemory, MemoryClient, AsyncMemoryClient +``` + +### Configure API Keys +Set up your environment variables in a `.env` file: + +```env +# Required +AGENTOPS_API_KEY=your_agentops_api_key +OPENAI_API_KEY=your_openai_api_key + +# Optional (for cloud operations) +MEM0_API_KEY=your_mem0_cloud_api_key +``` + +### Configuration +Set up configurations for local and cloud memory operations: + +```python +# Configuration for local memory (Memory and AsyncMemory) +local_config = { + "llm": { + "provider": "openai", + "config": { + "model": "gpt-4o-mini", + "temperature": 0.1, + "max_tokens": 2000, + "api_key": os.getenv("OPENAI_API_KEY"), + }, + } +} + +# API key for cloud clients +mem0_api_key = os.getenv("MEM0_API_KEY") + +# Sample data +user_id = "alice_demo" +sample_messages = [ + {"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"}, + {"role": "assistant", "content": "How about a thriller? They can be quite engaging."}, + {"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."}, + { + "role": "assistant", + "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future.", + }, +] + +sample_preferences = [ + "I prefer dark roast coffee over light roast", + "I exercise every morning at 6 AM", + "I'm vegetarian and avoid all meat products", + "I love reading science fiction novels", + "I work in software engineering", +] +``` + +## Sync Memory Operations (Local) + +Demonstrate synchronous local memory operations: + +```python +def demonstrate_sync_memory(): + """Demonstrate sync Memory class operations.""" + print("\n" + "=" * 60) + print("SYNC MEMORY (Local) OPERATIONS") + print("=" * 60) + + try: + # Initialize sync Memory + memory = Memory.from_config(local_config) + print("Sync Memory initialized successfully") + + # 1. ADD operations + print("\nAdding memories...") + + # Add conversation messages + result = memory.add( + sample_messages, + user_id=user_id, + metadata={"category": "movie_preferences", "session": "demo"} + ) + print("Added conversation successfully") + + # Add individual preferences + for i, preference in enumerate(sample_preferences): + result = memory.add( + preference, + user_id=user_id, + metadata={"type": "preference", "index": i} + ) + print(f"Added {len(sample_preferences)} preferences") + + # 2. SEARCH operations + print("\nSearching memories...") + search_queries = [ + "What movies does the user like?", + "What are the user's food preferences?", + "When does the user exercise?", + ] + + for query in search_queries: + results = memory.search(query, user_id=user_id) + result_count = len(results.get("results", [])) if results else 0 + print(f"Query: '{query}' - Found {result_count} results") + + # 3. GET_ALL operations + print("\nGetting all memories...") + all_memories = memory.get_all(user_id=user_id) + if all_memories and "results" in all_memories: + print(f"Total memories: {len(all_memories['results'])}") + + # Cleanup + print("\nCleaning up all memories...") + delete_all_result = memory.delete_all(user_id=user_id) + print("Memories cleaned up successfully") + + except Exception as e: + print(f"Sync Memory error: {e}") + +# Run sync memory demonstration +demonstrate_sync_memory() +``` + +## Async Memory Operations (Local) + +Demonstrate asynchronous local memory operations with concurrency: + +```python +async def demonstrate_async_memory(): + """Demonstrate async Memory class operations.""" + print("\n" + "=" * 60) + print("ASYNC MEMORY (Local) OPERATIONS") + print("=" * 60) + + try: + # Initialize async Memory + async_memory = AsyncMemory.from_config(local_config) + print("Async Memory initialized successfully") + + # 1. ADD operations + print("\nAdding memories asynchronously...") + + # Add conversation messages + result = await async_memory.add( + sample_messages, + user_id=user_id, + metadata={"category": "async_movie_preferences", "session": "async_demo"} + ) + print("Added conversation successfully") + + # Add preferences concurrently + async def add_preference(preference, index): + return await async_memory.add( + preference, + user_id=user_id, + metadata={"type": "async_preference", "index": index} + ) + + tasks = [add_preference(pref, i) for i, pref in enumerate(sample_preferences)] + results = await asyncio.gather(*tasks) + print(f"Added {len(results)} preferences concurrently") + + # 2. SEARCH operations + print("\nSearching memories asynchronously...") + search_queries = [ + "What movies does the user like?", + "What are the user's dietary restrictions?", + "What does the user do for work?", + ] + + async def search_memory(query): + return await async_memory.search(query, user_id=user_id), query + + search_tasks = [search_memory(query) for query in search_queries] + search_results = await asyncio.gather(*search_tasks) + + for result, query in search_results: + result_count = len(result.get("results", [])) if result else 0 + print(f"Query: '{query}' - Found {result_count} results") + + # 3. CONCURRENT operations + print("\nPerforming concurrent operations...") + + # Get specific memory, update it, and check history concurrently + first_memory_id = (await async_memory.get_all(user_id=user_id))["results"][0]["id"] + + get_task = async_memory.get(first_memory_id, user_id=user_id) + update_task = async_memory.update( + memory_id=first_memory_id, + text="Updated: User loves sci-fi and fantasy movies", + user_id=user_id + ) + history_task = async_memory.history(first_memory_id, user_id=user_id) + + get_result, update_result, history_result = await asyncio.gather( + get_task, update_task, history_task + ) + + print(f"Concurrent operations completed - History entries: {len(history_result.get('results', []))}") + + # Cleanup + print("\nCleaning up all async memories...") + delete_all_result = await async_memory.delete_all(user_id=user_id) + print("Async memories cleaned up successfully") + + except Exception as e: + print(f"Async Memory error: {e}") + +# Run async memory demonstration +asyncio.run(demonstrate_async_memory()) +``` + +## Sync Memory Client (Cloud) + +Demonstrate cloud-based memory operations: + +```python +def demonstrate_sync_memory_client(): + """Demonstrate sync MemoryClient operations.""" + print("\n" + "=" * 60) + print("SYNC MEMORY CLIENT (Cloud) OPERATIONS") + print("=" * 60) + + if not mem0_api_key: + print("MEM0_API_KEY not found. Skipping cloud operations.") + return + + try: + # Initialize sync MemoryClient + client = MemoryClient(api_key=mem0_api_key) + print("Sync MemoryClient initialized successfully") + + # Add memories to cloud + print("\nAdding memories to cloud...") + + cloud_result = client.add( + sample_messages, + user_id=user_id, + metadata={"source": "cloud_demo", "type": "conversation"} + ) + print("Added conversation to cloud") + + # Add preferences + for i, preference in enumerate(sample_preferences[:3]): # Limited for demo + result = client.add( + preference, + user_id=user_id, + metadata={"type": "cloud_preference", "index": i} + ) + print(f"Added {len(sample_preferences[:3])} preferences to cloud") + + # Search cloud memories + print("\nSearching cloud memories...") + search_result = client.search( + "What are the user's entertainment preferences?", + user_id=user_id + ) + result_count = len(search_result.get("results", [])) if search_result else 0 + print(f"Cloud search completed - Found {result_count} results") + + # Get all cloud memories + print("\nGetting all cloud memories...") + all_cloud_memories = client.get_all(user_id=user_id) + if all_cloud_memories and "results" in all_cloud_memories: + print(f"Total cloud memories: {len(all_cloud_memories['results'])}") + + # Cleanup cloud memories + print("\nCleaning up cloud memories...") + delete_all_result = client.delete_all(user_id=user_id) + print("Cloud memories cleaned up successfully") + + except Exception as e: + print(f"Sync MemoryClient error: {e}") + +# Run sync memory client demonstration +demonstrate_sync_memory_client() +``` + +## Async Memory Client (Cloud) + +Demonstrate asynchronous cloud operations: + +```python +async def demonstrate_async_memory_client(): + """Demonstrate async MemoryClient operations.""" + print("\n" + "=" * 60) + print("ASYNC MEMORY CLIENT (Cloud) OPERATIONS") + print("=" * 60) + + if not mem0_api_key: + print("MEM0_API_KEY not found. Skipping async cloud operations.") + return + + try: + # Initialize async MemoryClient + async_client = AsyncMemoryClient(api_key=mem0_api_key) + print("Async MemoryClient initialized successfully") + + # Add memories to cloud asynchronously + print("\nAdding memories to cloud asynchronously...") + + # Concurrent add operations + add_conversation_task = async_client.add( + sample_messages, + user_id=user_id, + metadata={"source": "async_cloud_demo", "type": "conversation"} + ) + + add_preference_tasks = [ + async_client.add( + pref, + user_id=user_id, + metadata={"type": "async_cloud_preference", "index": i} + ) + for i, pref in enumerate(sample_preferences[:3]) + ] + + # Execute all add operations concurrently + results = await asyncio.gather(add_conversation_task, *add_preference_tasks) + print(f"Added conversation and preferences: {len(results)} items") + + # Concurrent search operations + print("\nPerforming concurrent searches...") + search_queries = [ + "What movies does the user prefer?", + "What are the user's work habits?", + "What are the user's dietary preferences?", + ] + + search_tasks = [ + async_client.search(query, user_id=user_id) + for query in search_queries + ] + search_results = await asyncio.gather(*search_tasks) + + for i, result in enumerate(search_results): + print(f"Search {i+1} result: {len(result.get('results', []))} memories found") + + # Get all memories and perform operations + print("\nGetting all async cloud memories...") + all_memories = await async_client.get_all(user_id=user_id) + if all_memories and "results" in all_memories: + print(f"Total async cloud memories: {len(all_memories['results'])}") + + # Cleanup + print("\nCleaning up async cloud memories...") + delete_all_result = await async_client.delete_all(user_id=user_id) + print("Async cloud memories cleaned up successfully") + + except Exception as e: + print(f"Async MemoryClient error: {e}") + +# Run async memory client demonstration +asyncio.run(demonstrate_async_memory_client()) +``` + +## Complete Example + +Run all demonstrations in sequence: + +```python +async def main(): + """Main function to run all Mem0 demonstrations.""" + print("Starting Comprehensive Mem0 Example with AgentOps") + print("=" * 80) + + # Check environment + required_vars = ["AGENTOPS_API_KEY", "OPENAI_API_KEY"] + missing_vars = [var for var in required_vars if not os.getenv(var)] + + if missing_vars: + print(f"Missing required environment variables: {missing_vars}") + return + + print("Environment variables checked") + + # Run all demonstrations + print("\nRunning all memory demonstrations...") + + # Sync operations + demonstrate_sync_memory() + + # Async operations + await demonstrate_async_memory() + + # Cloud operations (if API key available) + demonstrate_sync_memory_client() + await demonstrate_async_memory_client() + + print("\nAll Mem0 demonstrations completed!") + print("Check your AgentOps dashboard for detailed traces and metrics.") + +# Run the complete example +if __name__ == "__main__": + asyncio.run(main()) +``` + +## What You'll See in AgentOps Dashboard + +After running this example, visit your [AgentOps Dashboard](https://app.agentops.ai/) to see: + +- **Memory Operations**: Detailed traces of all add, search, update, and delete operations +- **Performance Metrics**: Latency and success rates for each memory operation +- **User Tracking**: All operations organized by user_id for easy filtering +- **Metadata Analysis**: Custom metadata and tags for memory categorization +- **Error Monitoring**: Any failed operations with full stack traces and context +- **Search Analytics**: Query patterns and retrieval effectiveness metrics +- **Async Operations**: Concurrent operation tracking with timing details + +## Best Practices Demonstrated + +1. **Proper Initialization**: Initialize AgentOps and Mem0 +2. **Comprehensive Metadata**: All operations include meaningful metadata +3. **Async Patterns**: Demonstrates concurrent operations for better performance +4. **Error Handling**: Robust try-catch blocks for all operations +5. **Resource Cleanup**: Proper cleanup of memories after demonstrations +6. **User Isolation**: Consistent user_id usage for proper memory segmentation + + + + + \ No newline at end of file diff --git a/docs/v2/integrations/mem0.mdx b/docs/v2/integrations/mem0.mdx new file mode 100644 index 000000000..6b631b2d5 --- /dev/null +++ b/docs/v2/integrations/mem0.mdx @@ -0,0 +1,256 @@ +--- +title: Mem0 +description: "Track your Mem0 memory operations with AgentOps" +--- + +[Mem0](https://mem0.ai) is a framework for enabling long-term memory for AI agents. It provides personalized AI experiences by remembering user preferences, behaviors, and conversation context across sessions. AgentOps automatically tracks all Mem0 memory operations including storage, retrieval, updates, and deletions. + +## Installation + +Install AgentOps and Mem0: + + + ```bash pip + pip install agentops mem0ai + ``` + ```bash poetry + poetry add agentops mem0ai + ``` + ```bash uv + uv add agentops mem0ai + ``` + + +## Setting Up API Keys + +You'll need API keys for AgentOps and your chosen LLM provider: +- **AGENTOPS_API_KEY**: From your [AgentOps Dashboard](https://app.agentops.ai/) +- **OPENAI_API_KEY**: From the [OpenAI Platform](https://platform.openai.com/api-keys) (if using OpenAI) +- **MEM0_API_KEY**: From [Mem0 Platform](https://mem0.ai) (optional, for cloud operations) + +Set these as environment variables or in a `.env` file. + + + ```bash Export to CLI + export AGENTOPS_API_KEY="your_agentops_api_key_here" + export OPENAI_API_KEY="your_openai_api_key_here" + export MEM0_API_KEY="your_mem0_api_key_here" # Optional + ``` + ```txt Set in .env file + AGENTOPS_API_KEY="your_agentops_api_key_here" + OPENAI_API_KEY="your_openai_api_key_here" + MEM0_API_KEY="your_mem0_api_key_here" # Optional + ``` + + +Then load them in your Python code: +```python +from dotenv import load_dotenv +import os + +load_dotenv() + +AGENTOPS_API_KEY = os.getenv("AGENTOPS_API_KEY") +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +MEM0_API_KEY = os.getenv("MEM0_API_KEY") # Optional +``` + +### Local Memory Operations + +Use Mem0's local memory classes for on-device storage and retrieval: + +```python +import agentops +from mem0 import Memory, AsyncMemory + +# Initialize AgentOps +agentops.init(api_key=AGENTOPS_API_KEY) + +# Configure local memory +config = { + "llm": { + "provider": "openai", + "config": { + "model": "gpt-4o-mini", + "temperature": 0.1, + "max_tokens": 2000, + "api_key": OPENAI_API_KEY, + }, + } +} + +# Initialize memory +memory = Memory.from_config(config) + +# Add memories +user_id = "user_123" +messages = [ + {"role": "user", "content": "I love science fiction movies"}, + {"role": "assistant", "content": "I'll remember that you enjoy sci-fi films!"} +] + +result = memory.add(messages, user_id=user_id) +print(f"Memory added: {result}") + +# Search memories +search_results = memory.search("What movies does the user like?", user_id=user_id) +print(f"Search results: {search_results}") + +# Get all memories +all_memories = memory.get_all(user_id=user_id) +print(f"All memories: {all_memories}") +``` + +### Async Memory Operations + +For asynchronous operations with better performance: + +```python +import asyncio +import agentops +from mem0 import AsyncMemory + +agentops.init(api_key=AGENTOPS_API_KEY) + +async def async_memory_example(): + # Initialize async memory + async_memory = AsyncMemory.from_config(config) + + # Add memories asynchronously + result = await async_memory.add( + "I prefer dark roast coffee", + user_id=user_id, + metadata={"category": "preferences"} + ) + + # Search memories + results = await async_memory.search( + "What are the user's drink preferences?", + user_id=user_id + ) + + # Concurrent operations + tasks = [ + async_memory.add(f"Preference {i}", user_id=user_id) + for i in range(3) + ] + await asyncio.gather(*tasks) + + return results + +# Run async function +results = asyncio.run(async_memory_example()) +``` + +### Cloud Memory Operations + +Use Mem0's cloud service for managed memory operations: + +```python +import agentops +from mem0 import MemoryClient, AsyncMemoryClient + +agentops.init(api_key=AGENTOPS_API_KEY) + +# Sync cloud client +client = MemoryClient(api_key=MEM0_API_KEY) + +# Add to cloud +result = client.add( + "User prefers morning meetings", + user_id=user_id, + metadata={"type": "work_preference"} +) + +# Search cloud memories +search_results = client.search( + "When does the user prefer meetings?", + user_id=user_id +) + +# Async cloud client +async def async_cloud_example(): + async_client = AsyncMemoryClient(api_key=MEM0_API_KEY) + + # Concurrent cloud operations + add_task = async_client.add("Async cloud memory", user_id=user_id) + search_task = async_client.search("user preferences", user_id=user_id) + + add_result, search_result = await asyncio.gather(add_task, search_task) + return add_result, search_result + +# Run async cloud operations +asyncio.run(async_cloud_example()) +``` + +## Complete CRUD Operations + +Mem0 with AgentOps supports full CRUD (Create, Read, Update, Delete) operations: + +```python +import agentops +from mem0 import Memory + +agentops.init(api_key=AGENTOPS_API_KEY) +memory = Memory.from_config(config) + +# CREATE - Add new memory +add_result = memory.add( + "User is learning Python programming", + user_id=user_id, + metadata={"skill": "programming", "level": "beginner"} +) +memory_id = add_result.get("memory_id") + +# READ - Get specific memory +memory_details = memory.get(memory_id, user_id=user_id) + +# UPDATE - Modify existing memory +update_result = memory.update( + memory_id=memory_id, + text="User is now intermediate at Python programming", + user_id=user_id +) + +# DELETE - Remove specific memory +delete_result = memory.delete(memory_id=memory_id, user_id=user_id) + +# DELETE ALL - Clear all memories for user +delete_all_result = memory.delete_all(user_id=user_id) + +``` + + +## AgentOps Dashboard Features + +After running your Mem0 operations, visit the [AgentOps Dashboard](https://app.agentops.ai/) to see: + +- **Memory Operations**: Detailed traces of add, search, update, and delete operations +- **Performance Metrics**: Latency and success rates for memory operations +- **User Tracking**: Memory operations organized by user_id and agent_id +- **Metadata Analysis**: Custom metadata and tags for memory categorization +- **Error Monitoring**: Failed operations with full stack traces +- **Search Analytics**: Query patterns and retrieval effectiveness + +## Examples + + + + Complete example demonstrating all four Mem0 memory classes with AgentOps instrumentation. + + + +## Best Practices + +1. **Initialize AgentOps**: Always call `agentops.init()`. +2. **Use Metadata**: Add meaningful metadata to memories for better organization and filtering +3. **Handle Errors**: Wrap memory operations in try-catch blocks for robust error handling +4. **User Segmentation**: Use consistent user_id patterns for proper memory isolation +5. **Async Operations**: Use async classes for better performance in concurrent scenarios +6. **Memory Cleanup**: Regularly clean up old or irrelevant memories to maintain performance + + + + + + \ No newline at end of file diff --git a/examples/mem0_examples/comprehensive_mem0_example.py b/examples/mem0_examples/comprehensive_mem0_example.py index 4aba1687d..5dffd4616 100644 --- a/examples/mem0_examples/comprehensive_mem0_example.py +++ b/examples/mem0_examples/comprehensive_mem0_example.py @@ -21,12 +21,12 @@ # CRITICAL: Initialize AgentOps BEFORE importing mem0 classes # This ensures proper instrumentation and context propagation import agentops # noqa: E402 +from mem0 import Memory, AsyncMemory, MemoryClient, AsyncMemoryClient # noqa: E402 + -# Initialize AgentOps FIRST +# Initialize AgentOps agentops.init(os.getenv("AGENTOPS_API_KEY")) -# Now import mem0 classes AFTER agentops initialization -from mem0 import Memory, AsyncMemory, MemoryClient, AsyncMemoryClient # noqa: E402 # Set up logging logging.basicConfig(level=logging.INFO) From 16c8e7cdaa0fd1ca8ba37415edfb12136dc9463a Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Thu, 12 Jun 2025 06:52:57 +0530 Subject: [PATCH 15/23] removed the utility instrumentation from mem0 --- agentops/instrumentation/__init__.py | 176 +++++++----------- .../concurrent_futures/__init__.py | 10 - .../concurrent_futures/instrumentation.py | 174 ----------------- 3 files changed, 68 insertions(+), 292 deletions(-) delete mode 100644 agentops/instrumentation/concurrent_futures/__init__.py delete mode 100644 agentops/instrumentation/concurrent_futures/instrumentation.py diff --git a/agentops/instrumentation/__init__.py b/agentops/instrumentation/__init__.py index f9e1cce2f..86740a622 100644 --- a/agentops/instrumentation/__init__.py +++ b/agentops/instrumentation/__init__.py @@ -35,82 +35,6 @@ from agentops.logging import logger from agentops.sdk.core import tracer - -# Define the structure for instrumentor configurations -class InstrumentorConfig(TypedDict): - module_name: str - class_name: str - min_version: str - package_name: NotRequired[str] # Optional: actual pip package name if different from module - - -# Configuration for supported LLM providers -PROVIDERS: dict[str, InstrumentorConfig] = { - "openai": { - "module_name": "agentops.instrumentation.openai", - "class_name": "OpenAIInstrumentor", - "min_version": "1.0.0", - }, - "anthropic": { - "module_name": "agentops.instrumentation.anthropic", - "class_name": "AnthropicInstrumentor", - "min_version": "0.32.0", - }, - "ibm_watsonx_ai": { - "module_name": "agentops.instrumentation.ibm_watsonx_ai", - "class_name": "IBMWatsonXInstrumentor", - "min_version": "0.1.0", - }, - "google.genai": { - "module_name": "agentops.instrumentation.google_genai", - "class_name": "GoogleGenAIInstrumentor", - "min_version": "0.1.0", - "package_name": "google-genai", # Actual pip package name - }, - "mem0": { - "module_name": "agentops.instrumentation.mem0", - "class_name": "Mem0Instrumentor", - "min_version": "0.1.0", - "package_name": "mem0ai", # Actual pip package name - }, -} - -# Configuration for utility instrumentors -UTILITY_INSTRUMENTORS: dict[str, InstrumentorConfig] = { - "concurrent.futures": { - "module_name": "agentops.instrumentation.concurrent_futures", - "class_name": "ConcurrentFuturesInstrumentor", - "min_version": "3.7.0", # Python 3.7+ (concurrent.futures is stdlib) - "package_name": "python", # Special case for stdlib modules - }, -} - -# Configuration for supported agentic libraries -AGENTIC_LIBRARIES: dict[str, InstrumentorConfig] = { - "crewai": { - "module_name": "agentops.instrumentation.crewai", - "class_name": "CrewAIInstrumentor", - "min_version": "0.56.0", - }, - "autogen": {"module_name": "agentops.instrumentation.ag2", "class_name": "AG2Instrumentor", "min_version": "0.1.0"}, - "agents": { - "module_name": "agentops.instrumentation.openai_agents", - "class_name": "OpenAIAgentsInstrumentor", - "min_version": "0.0.1", - }, - "google.adk": { - "module_name": "agentops.instrumentation.google_adk", - "class_name": "GoogleADKInstrumentor", - "min_version": "0.1.0", - }, -} - -# Combine all target packages for monitoring -TARGET_PACKAGES = set(PROVIDERS.keys()) | set(AGENTIC_LIBRARIES.keys()) | set(UTILITY_INSTRUMENTORS.keys()) - -# Create a single instance of the manager -# _manager = InstrumentationManager() # Removed - # Module-level state variables _active_instrumentors: list[BaseInstrumentor] = [] _original_builtins_import = builtins.__import__ # Store original import @@ -125,16 +49,6 @@ def _is_installed_package(module_obj: ModuleType, package_name_key: str) -> bool rather than a local module, especially when names might collide. `package_name_key` is the key from TARGET_PACKAGES (e.g., 'agents', 'google.adk'). """ - # Special case for stdlib modules (marked with package_name="python" in UTILITY_INSTRUMENTORS) - if ( - package_name_key in UTILITY_INSTRUMENTORS - and UTILITY_INSTRUMENTORS[package_name_key].get("package_name") == "python" - ): - logger.debug( - f"_is_installed_package: Module '{package_name_key}' is a Python standard library module. Considering it an installed package." - ) - return True - if not hasattr(module_obj, "__file__") or not module_obj.__file__: logger.debug( f"_is_installed_package: Module '{package_name_key}' has no __file__, assuming it might be an SDK namespace package. Returning True." @@ -227,7 +141,7 @@ def _uninstrument_providers(): def _should_instrument_package(package_name: str) -> bool: """ Determine if a package should be instrumented based on current state. - Handles special cases for agentic libraries, providers, and utility instrumentors. + Handles special cases for agentic libraries and providers. """ global _has_agentic_library @@ -236,12 +150,6 @@ def _should_instrument_package(package_name: str) -> bool: logger.debug(f"_should_instrument_package: '{package_name}' already instrumented by AgentOps. Skipping.") return False - # Utility instrumentors should always be instrumented regardless of agentic library state - if package_name in UTILITY_INSTRUMENTORS: - logger.debug(f"_should_instrument_package: '{package_name}' is a utility instrumentor. Always allowing.") - return True - - # Only apply agentic/provider logic if it's NOT a utility instrumentor is_target_agentic = package_name in AGENTIC_LIBRARIES is_target_provider = package_name in PROVIDERS @@ -290,18 +198,14 @@ def _perform_instrumentation(package_name: str): return # Get the appropriate configuration for the package - # Ensure package_name is a key in either PROVIDERS, AGENTIC_LIBRARIES, or UTILITY_INSTRUMENTORS - if ( - package_name not in PROVIDERS - and package_name not in AGENTIC_LIBRARIES - and package_name not in UTILITY_INSTRUMENTORS - ): + # Ensure package_name is a key in either PROVIDERS or AGENTIC_LIBRARIES + if package_name not in PROVIDERS and package_name not in AGENTIC_LIBRARIES: logger.debug( - f"_perform_instrumentation: Package '{package_name}' not found in PROVIDERS, AGENTIC_LIBRARIES, or UTILITY_INSTRUMENTORS. Skipping." + f"_perform_instrumentation: Package '{package_name}' not found in PROVIDERS or AGENTIC_LIBRARIES. Skipping." ) return - config = PROVIDERS.get(package_name) or AGENTIC_LIBRARIES.get(package_name) or UTILITY_INSTRUMENTORS[package_name] + config = PROVIDERS.get(package_name) or AGENTIC_LIBRARIES[package_name] loader = InstrumentorLoader(**config) # instrument_one already checks loader.should_activate @@ -423,6 +327,69 @@ def _import_monitor(name: str, globals_dict=None, locals_dict=None, fromlist=(), return module +# Define the structure for instrumentor configurations +class InstrumentorConfig(TypedDict): + module_name: str + class_name: str + min_version: str + package_name: NotRequired[str] # Optional: actual pip package name if different from module + + +# Configuration for supported LLM providers +PROVIDERS: dict[str, InstrumentorConfig] = { + "openai": { + "module_name": "agentops.instrumentation.openai", + "class_name": "OpenAIInstrumentor", + "min_version": "1.0.0", + "package_name": "openai", # Actual pip package name + }, + "anthropic": { + "module_name": "agentops.instrumentation.anthropic", + "class_name": "AnthropicInstrumentor", + "min_version": "0.32.0", + "package_name": "anthropic", # Actual pip package name + }, + "ibm_watsonx_ai": { + "module_name": "agentops.instrumentation.ibm_watsonx_ai", + "class_name": "IBMWatsonXInstrumentor", + "min_version": "0.1.0", + "package_name": "ibm-watsonx-ai", # Actual pip package name + }, + "google.genai": { + "module_name": "agentops.instrumentation.google_genai", + "class_name": "GoogleGenAIInstrumentor", + "min_version": "0.1.0", + "package_name": "google-genai", # Actual pip package name + }, +} + +# Configuration for supported agentic libraries +AGENTIC_LIBRARIES: dict[str, InstrumentorConfig] = { + "crewai": { + "module_name": "agentops.instrumentation.crewai", + "class_name": "CrewAIInstrumentor", + "min_version": "0.56.0", + "package_name": "crewai", # Actual pip package name + }, + "autogen": {"module_name": "agentops.instrumentation.ag2", "class_name": "AG2Instrumentor", "min_version": "0.1.0"}, + "agents": { + "module_name": "agentops.instrumentation.openai_agents", + "class_name": "OpenAIAgentsInstrumentor", + "min_version": "0.0.1", + "package_name": "openai-agents", + }, + "google.adk": { + "module_name": "agentops.instrumentation.google_adk", + "class_name": "GoogleADKInstrumentor", + "min_version": "0.1.0", + "package_name": "google-adk", # Actual pip package name + }, +} + +# Combine all target packages for monitoring +TARGET_PACKAGES = set(PROVIDERS.keys()) | set(AGENTIC_LIBRARIES.keys()) + + @dataclass class InstrumentorLoader: """ @@ -444,13 +411,6 @@ def module(self) -> ModuleType: def should_activate(self) -> bool: """Check if the package is available and meets version requirements.""" try: - # Special case for stdlib modules (like concurrent.futures) - if self.package_name == "python": - import sys - - python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" - return Version(python_version) >= parse(self.min_version) - # Use explicit package_name if provided, otherwise derive from module_name if self.package_name: provider_name = self.package_name diff --git a/agentops/instrumentation/concurrent_futures/__init__.py b/agentops/instrumentation/concurrent_futures/__init__.py deleted file mode 100644 index 943fd5b0b..000000000 --- a/agentops/instrumentation/concurrent_futures/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Instrumentation for concurrent.futures module. - -This module provides automatic instrumentation for ThreadPoolExecutor to ensure -proper OpenTelemetry context propagation across thread boundaries. -""" - -from .instrumentation import ConcurrentFuturesInstrumentor - -__all__ = ["ConcurrentFuturesInstrumentor"] diff --git a/agentops/instrumentation/concurrent_futures/instrumentation.py b/agentops/instrumentation/concurrent_futures/instrumentation.py deleted file mode 100644 index 71c9b50f0..000000000 --- a/agentops/instrumentation/concurrent_futures/instrumentation.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -OpenTelemetry Instrumentation for concurrent.futures module. - -This instrumentation automatically patches ThreadPoolExecutor to ensure proper -context propagation across thread boundaries, preventing "NEW TRACE DETECTED" issues. -""" - -import contextvars -import functools -from typing import Any, Callable, Collection, Optional, Tuple, TypeVar - -from concurrent.futures import ThreadPoolExecutor, Future - -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor - -from agentops.logging import logger - -# Store original methods to restore during uninstrumentation -_original_init = None -_original_submit = None - -# Type variables for better typing -T = TypeVar("T") -R = TypeVar("R") - - -def _context_propagating_init(original_init: Callable) -> Callable: - """Wrap ThreadPoolExecutor.__init__ to set up context-aware initializer.""" - - @functools.wraps(original_init) - def wrapped_init( - self: ThreadPoolExecutor, - max_workers: Optional[int] = None, - thread_name_prefix: str = "", - initializer: Optional[Callable] = None, - initargs: Tuple = (), - ) -> None: - # Capture the current context when the executor is created - main_context = contextvars.copy_context() - - def context_aware_initializer() -> None: - """Initializer that sets up the captured context in each worker thread.""" - logger.debug("[ConcurrentFuturesInstrumentor] Setting up context in worker thread") - - # Set the main context variables in this thread - for var, value in main_context.items(): - try: - var.set(value) - except Exception as e: - logger.debug(f"[ConcurrentFuturesInstrumentor] Could not set context var {var}: {e}") - - # Run user's initializer if provided - if initializer and callable(initializer): - try: - if initargs: - initializer(*initargs) - else: - initializer() - except Exception as e: - logger.error(f"[ConcurrentFuturesInstrumentor] Error in user initializer: {e}") - raise - - logger.debug("[ConcurrentFuturesInstrumentor] Worker thread context setup complete") - - # Create executor with context-aware initializer - prefix = f"AgentOps-{thread_name_prefix}" if thread_name_prefix else "AgentOps-Thread" - - # Call original init with our context-aware initializer - original_init( - self, - max_workers=max_workers, - thread_name_prefix=prefix, - initializer=context_aware_initializer, - initargs=(), # We handle initargs in our wrapper - ) - - logger.debug("[ConcurrentFuturesInstrumentor] ThreadPoolExecutor initialized with context propagation") - - return wrapped_init - - -def _context_propagating_submit(original_submit: Callable) -> Callable: - """Wrap ThreadPoolExecutor.submit to ensure context propagation.""" - - @functools.wraps(original_submit) - def wrapped_submit(self: ThreadPoolExecutor, func: Callable[..., R], *args: Any, **kwargs: Any) -> Future[R]: - # Log the submission - func_name = getattr(func, "__name__", str(func)) - logger.debug(f"[ConcurrentFuturesInstrumentor] Submitting function: {func_name}") - - # The context propagation is handled by the initializer, so we can submit normally - # But we can add additional logging or monitoring here if needed - return original_submit(self, func, *args, **kwargs) - - return wrapped_submit - - -class ConcurrentFuturesInstrumentor(BaseInstrumentor): - """ - Instrumentor for concurrent.futures module. - - This instrumentor patches ThreadPoolExecutor to automatically propagate - OpenTelemetry context to worker threads, ensuring all LLM calls and other - instrumented operations maintain proper trace context. - """ - - def instrumentation_dependencies(self) -> Collection[str]: - """Return a list of instrumentation dependencies.""" - return [] - - def _instrument(self, **kwargs: Any) -> None: - """Instrument the concurrent.futures module.""" - global _original_init, _original_submit - - logger.debug("[ConcurrentFuturesInstrumentor] Starting instrumentation") - - # Store original methods - _original_init = ThreadPoolExecutor.__init__ - _original_submit = ThreadPoolExecutor.submit - - # Patch ThreadPoolExecutor methods - ThreadPoolExecutor.__init__ = _context_propagating_init(_original_init) - ThreadPoolExecutor.submit = _context_propagating_submit(_original_submit) - - logger.info("[ConcurrentFuturesInstrumentor] Successfully instrumented concurrent.futures.ThreadPoolExecutor") - - def _uninstrument(self, **kwargs: Any) -> None: - """Uninstrument the concurrent.futures module.""" - global _original_init, _original_submit - - logger.debug("[ConcurrentFuturesInstrumentor] Starting uninstrumentation") - - # Restore original methods - if _original_init: - ThreadPoolExecutor.__init__ = _original_init - _original_init = None - - if _original_submit: - ThreadPoolExecutor.submit = _original_submit - _original_submit = None - - logger.info("[ConcurrentFuturesInstrumentor] Successfully uninstrumented concurrent.futures.ThreadPoolExecutor") - - @staticmethod - def instrument_module_directly() -> bool: - """ - Directly instrument the module without using the standard instrumentor interface. - - This can be called manually if automatic instrumentation is not desired. - - Returns: - bool: True if instrumentation was applied, False if already instrumented - """ - instrumentor = ConcurrentFuturesInstrumentor() - if not instrumentor.is_instrumented_by_opentelemetry: - instrumentor.instrument() - return True - return False - - @staticmethod - def uninstrument_module_directly() -> bool: - """ - Directly uninstrument the module. - - This can be called manually to remove instrumentation. - - Returns: - bool: True if uninstrumentation was applied, False if already uninstrumented - """ - instrumentor = ConcurrentFuturesInstrumentor() - if instrumentor.is_instrumented_by_opentelemetry: - instrumentor.uninstrument() - return True - return False From d8be14a2746c3f2e804b3b995d72943a9bfc02fa Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Fri, 13 Jun 2025 20:39:30 +0530 Subject: [PATCH 16/23] v2 docs added --- agentops/instrumentation/__init__.py | 6 + docs/v2/examples/mem0.mdx | 439 ++++---------- docs/v2/integrations/mem0.mdx | 301 ++++------ docs/v2/introduction.mdx | 1 + examples/mem0/mem0_memory_example.ipynb | 322 ++++++++++ examples/mem0/mem0_memory_example.py | 225 +++++++ examples/mem0/mem0_memoryclient_example.ipynb | 554 ++++++++++++++++++ examples/mem0/mem0_memoryclient_example.py | 186 ++++++ examples/mem0_examples/README.md | 227 ------- .../comprehensive_mem0_example.py | 373 ------------ 10 files changed, 1496 insertions(+), 1138 deletions(-) create mode 100644 examples/mem0/mem0_memory_example.ipynb create mode 100644 examples/mem0/mem0_memory_example.py create mode 100644 examples/mem0/mem0_memoryclient_example.ipynb create mode 100644 examples/mem0/mem0_memoryclient_example.py delete mode 100644 examples/mem0_examples/README.md delete mode 100644 examples/mem0_examples/comprehensive_mem0_example.py diff --git a/agentops/instrumentation/__init__.py b/agentops/instrumentation/__init__.py index e45995144..84ace1d6d 100644 --- a/agentops/instrumentation/__init__.py +++ b/agentops/instrumentation/__init__.py @@ -67,6 +67,12 @@ class InstrumentorConfig(TypedDict): "min_version": "0.1.0", "package_name": "google-genai", # Actual pip package name }, + "mem0": { + "module_name": "agentops.instrumentation.mem0", + "class_name": "Mem0Instrumentor", + "min_version": "0.1.0", + "package_name": "mem0ai", + }, } # Configuration for utility instrumentors diff --git a/docs/v2/examples/mem0.mdx b/docs/v2/examples/mem0.mdx index 216d1a58e..0ce76d2dc 100644 --- a/docs/v2/examples/mem0.mdx +++ b/docs/v2/examples/mem0.mdx @@ -1,75 +1,76 @@ --- -title: 'Mem0 Example' -description: 'Comprehensive Mem0 memory operations with AgentOps tracking' +title: 'Mem0' +description: 'Memory Operations with Mem0' --- +{/* SOURCE_FILE: examples/mem0/mem0_memory_example.ipynb */} -_View Example on Github_ +_View Notebook on Github_ -# AgentOps Mem0 Integration +# Memory Operations with Mem0 + +This example demonstrates how to use Mem0's memory management capabilities with both synchronous and asynchronous operations to store, search, and manage conversational context and user preferences. + +## Overview + +This example showcases practical memory management operations where we: + +1. **Initialize Mem0 Memory instances** for both sync and async operations +2. **Store conversation history** and user preferences with metadata +3. **Search memories** using natural language queries +4. **Compare performance** between synchronous and asynchronous memory operations + +By using async operations, you can perform multiple memory operations simultaneously instead of waiting for each one to complete sequentially. This is particularly beneficial when dealing with multiple memory additions or searches. -This comprehensive example demonstrates all four Mem0 memory classes with complete AgentOps instrumentation: -- **Memory** (Sync Local Memory) -- **AsyncMemory** (Async Local Memory) -- **MemoryClient** (Sync Cloud Client) -- **AsyncMemoryClient** (Async Cloud Client) -## Installation -Install the required packages: +## Setup and Imports +## Installation -```bash pip -pip install agentops mem0ai python-dotenv -``` -```bash poetry -poetry add agentops mem0ai python-dotenv -``` -```bash uv -uv add agentops mem0ai python-dotenv -``` + ```bash pip + pip install agentops mem0ai python-dotenv + ``` + ```bash poetry + poetry add agentops mem0ai python-dotenv + ``` + ```bash uv + uv add agentops mem0ai python-dotenv + ``` -## Setup -### Import Dependencies -Import the necessary libraries: +Import the required libraries for local memory management with Mem0. We'll use both Memory and AsyncMemory classes to demonstrate different execution patterns for memory operations. + + ```python + +from mem0 import Memory, AsyncMemory import os import asyncio import logging from dotenv import load_dotenv +import agentops +``` -# Load environment variables -load_dotenv() +## Environment Configuration + +Set up environment variables for API keys. These are essential for authenticating with AgentOps for tracing and OpenAI for the language model used in memory operations. -# This ensures proper instrumentation -import agentops -# Initialize AgentOps -agentops.init(os.getenv("AGENTOPS_API_KEY")) -# Now import mem0 -from mem0 import Memory, AsyncMemory, MemoryClient, AsyncMemoryClient +```python +os.environ["AGENTOPS_API_KEY"] = os.getenv("AGENTOPS_API_KEY") +os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY") ``` -### Configure API Keys -Set up your environment variables in a `.env` file: +## Configuration and Sample Data -```env -# Required -AGENTOPS_API_KEY=your_agentops_api_key -OPENAI_API_KEY=your_openai_api_key +Set up the configuration for local memory storage and define sample user data. The configuration specifies the LLM provider and model settings for processing memories. -# Optional (for cloud operations) -MEM0_API_KEY=your_mem0_cloud_api_key -``` -### Configuration -Set up configurations for local and cloud memory operations: ```python -# Configuration for local memory (Memory and AsyncMemory) local_config = { "llm": { "provider": "openai", @@ -77,16 +78,13 @@ local_config = { "model": "gpt-4o-mini", "temperature": 0.1, "max_tokens": 2000, - "api_key": os.getenv("OPENAI_API_KEY"), }, } } - -# API key for cloud clients -mem0_api_key = os.getenv("MEM0_API_KEY") - -# Sample data user_id = "alice_demo" +agent_id = "assistant_demo" +run_id = "session_001" + sample_messages = [ {"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"}, {"role": "assistant", "content": "How about a thriller? They can be quite engaging."}, @@ -106,44 +104,30 @@ sample_preferences = [ ] ``` -## Sync Memory Operations (Local) +## Synchronous Memory Operations + +This function demonstrates sequential memory operations using the synchronous Memory class. While straightforward to implement, each operation must complete before the next begins, which can impact performance. + -Demonstrate synchronous local memory operations: ```python -def demonstrate_sync_memory(): - """Demonstrate sync Memory class operations.""" - print("\n" + "=" * 60) - print("SYNC MEMORY (Local) OPERATIONS") - print("=" * 60) +def demonstrate_sync_memory(local_config, sample_messages, sample_preferences, user_id): + """ + Demonstrate synchronous Memory class operations. + """ + agentops.start_trace("mem0_memory_example", tags=["mem0_memory_example"]) try: - # Initialize sync Memory + memory = Memory.from_config(local_config) - print("Sync Memory initialized successfully") - # 1. ADD operations - print("\nAdding memories...") - - # Add conversation messages result = memory.add( - sample_messages, - user_id=user_id, - metadata={"category": "movie_preferences", "session": "demo"} + sample_messages, user_id=user_id, metadata={"category": "movie_preferences", "session": "demo"} ) - print("Added conversation successfully") - # Add individual preferences for i, preference in enumerate(sample_preferences): - result = memory.add( - preference, - user_id=user_id, - metadata={"type": "preference", "index": i} - ) - print(f"Added {len(sample_preferences)} preferences") - - # 2. SEARCH operations - print("\nSearching memories...") + result = memory.add(preference, user_id=user_id, metadata={"type": "preference", "index": i}) + search_queries = [ "What movies does the user like?", "What are the user's food preferences?", @@ -152,68 +136,57 @@ def demonstrate_sync_memory(): for query in search_queries: results = memory.search(query, user_id=user_id) - result_count = len(results.get("results", [])) if results else 0 - print(f"Query: '{query}' - Found {result_count} results") + + if results and "results" in results: + for j, result in enumerate(results): + print(f"Result {j+1}: {result.get('memory', 'N/A')}") + else: + print("No results found") - # 3. GET_ALL operations - print("\nGetting all memories...") all_memories = memory.get_all(user_id=user_id) if all_memories and "results" in all_memories: print(f"Total memories: {len(all_memories['results'])}") - # Cleanup - print("\nCleaning up all memories...") delete_all_result = memory.delete_all(user_id=user_id) - print("Memories cleaned up successfully") + print(f"Delete all result: {delete_all_result}") + agentops.end_trace(end_state="success") except Exception as e: - print(f"Sync Memory error: {e}") - -# Run sync memory demonstration -demonstrate_sync_memory() + agentops.end_trace(end_state="error") ``` -## Async Memory Operations (Local) +## Asynchronous Memory Operations + +This function showcases concurrent memory operations using AsyncMemory. By leveraging asyncio.gather(), multiple operations execute simultaneously, significantly reducing total execution time for I/O-bound tasks. + -Demonstrate asynchronous local memory operations with concurrency: ```python -async def demonstrate_async_memory(): - """Demonstrate async Memory class operations.""" - print("\n" + "=" * 60) - print("ASYNC MEMORY (Local) OPERATIONS") - print("=" * 60) +async def demonstrate_async_memory(local_config, sample_messages, sample_preferences, user_id): + """ + Demonstrate asynchronous Memory class operations with concurrent execution. + """ + agentops.start_trace("mem0_memory_async_example", tags=["mem0_memory_async_example"]) try: - # Initialize async Memory - async_memory = AsyncMemory.from_config(local_config) - print("Async Memory initialized successfully") - # 1. ADD operations - print("\nAdding memories asynchronously...") + async_memory = await AsyncMemory.from_config(local_config) - # Add conversation messages result = await async_memory.add( - sample_messages, - user_id=user_id, - metadata={"category": "async_movie_preferences", "session": "async_demo"} + sample_messages, user_id=user_id, metadata={"category": "async_movie_preferences", "session": "async_demo"} ) - print("Added conversation successfully") - # Add preferences concurrently async def add_preference(preference, index): + """Helper function to add a single preference asynchronously.""" return await async_memory.add( - preference, - user_id=user_id, - metadata={"type": "async_preference", "index": index} + preference, user_id=user_id, metadata={"type": "async_preference", "index": index} ) tasks = [add_preference(pref, i) for i, pref in enumerate(sample_preferences)] results = await asyncio.gather(*tasks) - print(f"Added {len(results)} preferences concurrently") + for i, result in enumerate(results): + print(f"Added async preference {i+1}: {result}") - # 2. SEARCH operations - print("\nSearching memories asynchronously...") search_queries = [ "What movies does the user like?", "What are the user's dietary restrictions?", @@ -221,254 +194,46 @@ async def demonstrate_async_memory(): ] async def search_memory(query): + """Helper function to perform async memory search.""" return await async_memory.search(query, user_id=user_id), query search_tasks = [search_memory(query) for query in search_queries] search_results = await asyncio.gather(*search_tasks) for result, query in search_results: - result_count = len(result.get("results", [])) if result else 0 - print(f"Query: '{query}' - Found {result_count} results") + if result and "results" in result: + for j, res in enumerate(result["results"]): + print(f"Result {j+1}: {res.get('memory', 'N/A')}") + else: + print("No results found") - # 3. CONCURRENT operations - print("\nPerforming concurrent operations...") - - # Get specific memory, update it, and check history concurrently - first_memory_id = (await async_memory.get_all(user_id=user_id))["results"][0]["id"] - - get_task = async_memory.get(first_memory_id, user_id=user_id) - update_task = async_memory.update( - memory_id=first_memory_id, - text="Updated: User loves sci-fi and fantasy movies", - user_id=user_id - ) - history_task = async_memory.history(first_memory_id, user_id=user_id) - - get_result, update_result, history_result = await asyncio.gather( - get_task, update_task, history_task - ) - - print(f"Concurrent operations completed - History entries: {len(history_result.get('results', []))}") + all_memories = await async_memory.get_all(user_id=user_id) + if all_memories and "results" in all_memories: + print(f"Total async memories: {len(all_memories['results'])}") - # Cleanup - print("\nCleaning up all async memories...") delete_all_result = await async_memory.delete_all(user_id=user_id) - print("Async memories cleaned up successfully") - - except Exception as e: - print(f"Async Memory error: {e}") - -# Run async memory demonstration -asyncio.run(demonstrate_async_memory()) -``` - -## Sync Memory Client (Cloud) + print(f"Delete all result: {delete_all_result}") -Demonstrate cloud-based memory operations: - -```python -def demonstrate_sync_memory_client(): - """Demonstrate sync MemoryClient operations.""" - print("\n" + "=" * 60) - print("SYNC MEMORY CLIENT (Cloud) OPERATIONS") - print("=" * 60) - - if not mem0_api_key: - print("MEM0_API_KEY not found. Skipping cloud operations.") - return - - try: - # Initialize sync MemoryClient - client = MemoryClient(api_key=mem0_api_key) - print("Sync MemoryClient initialized successfully") - - # Add memories to cloud - print("\nAdding memories to cloud...") - - cloud_result = client.add( - sample_messages, - user_id=user_id, - metadata={"source": "cloud_demo", "type": "conversation"} - ) - print("Added conversation to cloud") - - # Add preferences - for i, preference in enumerate(sample_preferences[:3]): # Limited for demo - result = client.add( - preference, - user_id=user_id, - metadata={"type": "cloud_preference", "index": i} - ) - print(f"Added {len(sample_preferences[:3])} preferences to cloud") - - # Search cloud memories - print("\nSearching cloud memories...") - search_result = client.search( - "What are the user's entertainment preferences?", - user_id=user_id - ) - result_count = len(search_result.get("results", [])) if search_result else 0 - print(f"Cloud search completed - Found {result_count} results") - - # Get all cloud memories - print("\nGetting all cloud memories...") - all_cloud_memories = client.get_all(user_id=user_id) - if all_cloud_memories and "results" in all_cloud_memories: - print(f"Total cloud memories: {len(all_cloud_memories['results'])}") - - # Cleanup cloud memories - print("\nCleaning up cloud memories...") - delete_all_result = client.delete_all(user_id=user_id) - print("Cloud memories cleaned up successfully") + agentops.end_trace(end_state="success") except Exception as e: - print(f"Sync MemoryClient error: {e}") - -# Run sync memory client demonstration -demonstrate_sync_memory_client() + agentops.end_trace(end_state="error") ``` -## Async Memory Client (Cloud) - -Demonstrate asynchronous cloud operations: - -```python -async def demonstrate_async_memory_client(): - """Demonstrate async MemoryClient operations.""" - print("\n" + "=" * 60) - print("ASYNC MEMORY CLIENT (Cloud) OPERATIONS") - print("=" * 60) - - if not mem0_api_key: - print("MEM0_API_KEY not found. Skipping async cloud operations.") - return - - try: - # Initialize async MemoryClient - async_client = AsyncMemoryClient(api_key=mem0_api_key) - print("Async MemoryClient initialized successfully") - - # Add memories to cloud asynchronously - print("\nAdding memories to cloud asynchronously...") - - # Concurrent add operations - add_conversation_task = async_client.add( - sample_messages, - user_id=user_id, - metadata={"source": "async_cloud_demo", "type": "conversation"} - ) - - add_preference_tasks = [ - async_client.add( - pref, - user_id=user_id, - metadata={"type": "async_cloud_preference", "index": i} - ) - for i, pref in enumerate(sample_preferences[:3]) - ] - - # Execute all add operations concurrently - results = await asyncio.gather(add_conversation_task, *add_preference_tasks) - print(f"Added conversation and preferences: {len(results)} items") - - # Concurrent search operations - print("\nPerforming concurrent searches...") - search_queries = [ - "What movies does the user prefer?", - "What are the user's work habits?", - "What are the user's dietary preferences?", - ] - - search_tasks = [ - async_client.search(query, user_id=user_id) - for query in search_queries - ] - search_results = await asyncio.gather(*search_tasks) - - for i, result in enumerate(search_results): - print(f"Search {i+1} result: {len(result.get('results', []))} memories found") - - # Get all memories and perform operations - print("\nGetting all async cloud memories...") - all_memories = await async_client.get_all(user_id=user_id) - if all_memories and "results" in all_memories: - print(f"Total async cloud memories: {len(all_memories['results'])}") - - # Cleanup - print("\nCleaning up async cloud memories...") - delete_all_result = await async_client.delete_all(user_id=user_id) - print("Async cloud memories cleaned up successfully") +## Execute Demonstrations - except Exception as e: - print(f"Async MemoryClient error: {e}") +Run both synchronous and asynchronous demonstrations to compare their execution patterns and performance. The async version demonstrates the benefits of concurrent execution for multiple memory operations. -# Run async memory client demonstration -asyncio.run(demonstrate_async_memory_client()) -``` -## Complete Example - -Run all demonstrations in sequence: ```python -async def main(): - """Main function to run all Mem0 demonstrations.""" - print("Starting Comprehensive Mem0 Example with AgentOps") - print("=" * 80) - - # Check environment - required_vars = ["AGENTOPS_API_KEY", "OPENAI_API_KEY"] - missing_vars = [var for var in required_vars if not os.getenv(var)] - - if missing_vars: - print(f"Missing required environment variables: {missing_vars}") - return - - print("Environment variables checked") - - # Run all demonstrations - print("\nRunning all memory demonstrations...") - - # Sync operations - demonstrate_sync_memory() - - # Async operations - await demonstrate_async_memory() - - # Cloud operations (if API key available) - demonstrate_sync_memory_client() - await demonstrate_async_memory_client() - - print("\nAll Mem0 demonstrations completed!") - print("Check your AgentOps dashboard for detailed traces and metrics.") - -# Run the complete example -if __name__ == "__main__": - asyncio.run(main()) +# Execute both sync and async demonstrations +demonstrate_sync_memory(local_config, sample_messages, sample_preferences, user_id) +await demonstrate_async_memory(local_config, sample_messages, sample_preferences, user_id) ``` -## What You'll See in AgentOps Dashboard - -After running this example, visit your [AgentOps Dashboard](https://app.agentops.ai/) to see: - -- **Memory Operations**: Detailed traces of all add, search, update, and delete operations -- **Performance Metrics**: Latency and success rates for each memory operation -- **User Tracking**: All operations organized by user_id for easy filtering -- **Metadata Analysis**: Custom metadata and tags for memory categorization -- **Error Monitoring**: Any failed operations with full stack traces and context -- **Search Analytics**: Query patterns and retrieval effectiveness metrics -- **Async Operations**: Concurrent operation tracking with timing details - -## Best Practices Demonstrated - -1. **Proper Initialization**: Initialize AgentOps and Mem0 -2. **Comprehensive Metadata**: All operations include meaningful metadata -3. **Async Patterns**: Demonstrates concurrent operations for better performance -4. **Error Handling**: Robust try-catch blocks for all operations -5. **Resource Cleanup**: Proper cleanup of memories after demonstrations -6. **User Isolation**: Consistent user_id usage for proper memory segmentation - \ No newline at end of file + \ No newline at end of file diff --git a/docs/v2/integrations/mem0.mdx b/docs/v2/integrations/mem0.mdx index 6b631b2d5..873b69a5d 100644 --- a/docs/v2/integrations/mem0.mdx +++ b/docs/v2/integrations/mem0.mdx @@ -1,256 +1,155 @@ --- -title: Mem0 -description: "Track your Mem0 memory operations with AgentOps" +title: 'Mem0' +description: 'Track and monitor Mem0 memory operations with AgentOps' --- -[Mem0](https://mem0.ai) is a framework for enabling long-term memory for AI agents. It provides personalized AI experiences by remembering user preferences, behaviors, and conversation context across sessions. AgentOps automatically tracks all Mem0 memory operations including storage, retrieval, updates, and deletions. +[Mem0](https://mem0.ai/) provides a smart memory layer for AI applications, enabling personalized interactions by remembering user preferences, conversation history, and context across sessions. -## Installation +## Why Track Mem0 with AgentOps? + +When building memory-powered AI applications, you need visibility into: +- **Memory Operations**: Track when memories are created, updated, or retrieved +- **Search Performance**: Monitor how effectively your AI finds relevant memories +- **Memory Usage Patterns**: Understand what information is being stored and accessed +- **Error Tracking**: Identify issues with memory storage or retrieval +- **Cost Analysis**: Track API calls to both Mem0 and your LLM provider -Install AgentOps and Mem0: +AgentOps automatically instruments Mem0 to provide complete observability of your memory operations. + +## Installation - ```bash pip - pip install agentops mem0ai - ``` - ```bash poetry - poetry add agentops mem0ai - ``` - ```bash uv - uv add agentops mem0ai - ``` - +```bash pip +pip install agentops mem0ai python-dotenv +``` -## Setting Up API Keys +```bash poetry +poetry add agentops mem0ai python-dotenv +``` -You'll need API keys for AgentOps and your chosen LLM provider: -- **AGENTOPS_API_KEY**: From your [AgentOps Dashboard](https://app.agentops.ai/) -- **OPENAI_API_KEY**: From the [OpenAI Platform](https://platform.openai.com/api-keys) (if using OpenAI) -- **MEM0_API_KEY**: From [Mem0 Platform](https://mem0.ai) (optional, for cloud operations) +```bash uv +uv add agentops mem0ai python-dotenv +``` + -Set these as environment variables or in a `.env` file. +## Environment Configuration +Load environment variables and set up API keys. The MEM0_API_KEY is only required if you're using the cloud-based MemoryClient. ```bash Export to CLI export AGENTOPS_API_KEY="your_agentops_api_key_here" export OPENAI_API_KEY="your_openai_api_key_here" - export MEM0_API_KEY="your_mem0_api_key_here" # Optional ``` ```txt Set in .env file AGENTOPS_API_KEY="your_agentops_api_key_here" OPENAI_API_KEY="your_openai_api_key_here" - MEM0_API_KEY="your_mem0_api_key_here" # Optional ``` -Then load them in your Python code: -```python -from dotenv import load_dotenv -import os - -load_dotenv() - -AGENTOPS_API_KEY = os.getenv("AGENTOPS_API_KEY") -OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") -MEM0_API_KEY = os.getenv("MEM0_API_KEY") # Optional -``` - -### Local Memory Operations - -Use Mem0's local memory classes for on-device storage and retrieval: - -```python -import agentops -from mem0 import Memory, AsyncMemory - -# Initialize AgentOps -agentops.init(api_key=AGENTOPS_API_KEY) - -# Configure local memory -config = { - "llm": { - "provider": "openai", - "config": { - "model": "gpt-4o-mini", - "temperature": 0.1, - "max_tokens": 2000, - "api_key": OPENAI_API_KEY, - }, - } -} - -# Initialize memory -memory = Memory.from_config(config) - -# Add memories -user_id = "user_123" -messages = [ - {"role": "user", "content": "I love science fiction movies"}, - {"role": "assistant", "content": "I'll remember that you enjoy sci-fi films!"} -] - -result = memory.add(messages, user_id=user_id) -print(f"Memory added: {result}") - -# Search memories -search_results = memory.search("What movies does the user like?", user_id=user_id) -print(f"Search results: {search_results}") - -# Get all memories -all_memories = memory.get_all(user_id=user_id) -print(f"All memories: {all_memories}") -``` - -### Async Memory Operations - -For asynchronous operations with better performance: +## Tracking Memory Operations +#### Local Memory with AgentOps +AgentOps automatically instruments Local Memory Mem0 methods: ```python -import asyncio import agentops -from mem0 import AsyncMemory - -agentops.init(api_key=AGENTOPS_API_KEY) +from mem0 import Memory -async def async_memory_example(): - # Initialize async memory - async_memory = AsyncMemory.from_config(config) - - # Add memories asynchronously - result = await async_memory.add( - "I prefer dark roast coffee", - user_id=user_id, +# Start a trace to group related operations +agentops.start_trace("user_preference_learning",tags=["mem0_memory_example"]) + +try: + # Initialize Memory - AgentOps tracks the configuration + memory = Memory.from_config({ + "llm": { + "provider": "openai", + "config": { + "model": "gpt-4o-mini", + "temperature": 0.1 + } + } + }) + + # Add memories - AgentOps tracks each operation + memory.add( + "I prefer morning meetings and dark roast coffee", + user_id="user_123", metadata={"category": "preferences"} ) - - # Search memories - results = await async_memory.search( - "What are the user's drink preferences?", - user_id=user_id - ) - - # Concurrent operations - tasks = [ - async_memory.add(f"Preference {i}", user_id=user_id) - for i in range(3) - ] - await asyncio.gather(*tasks) - - return results - -# Run async function -results = asyncio.run(async_memory_example()) -``` - -### Cloud Memory Operations - -Use Mem0's cloud service for managed memory operations: - -```python -import agentops -from mem0 import MemoryClient, AsyncMemoryClient - -agentops.init(api_key=AGENTOPS_API_KEY) - -# Sync cloud client -client = MemoryClient(api_key=MEM0_API_KEY) - -# Add to cloud -result = client.add( - "User prefers morning meetings", - user_id=user_id, - metadata={"type": "work_preference"} -) -# Search cloud memories -search_results = client.search( - "When does the user prefer meetings?", - user_id=user_id -) + # Search memories - AgentOps tracks search queries and results + results = memory.search( + "What are the user's meeting preferences?", + user_id="user_123" + ) -# Async cloud client -async def async_cloud_example(): - async_client = AsyncMemoryClient(api_key=MEM0_API_KEY) - - # Concurrent cloud operations - add_task = async_client.add("Async cloud memory", user_id=user_id) - search_task = async_client.search("user preferences", user_id=user_id) + # End trace - AgentOps aggregates all operations + agentops.end_trace(end_state="success") - add_result, search_result = await asyncio.gather(add_task, search_task) - return add_result, search_result - -# Run async cloud operations -asyncio.run(async_cloud_example()) +except Exception as e: + agentops.end_trace(end_state="error") ``` -## Complete CRUD Operations - -Mem0 with AgentOps supports full CRUD (Create, Read, Update, Delete) operations: +#### Cloud Memory with agentops +AgentOps automatically instruments Cloud Memory Mem0 methods: ```python import agentops -from mem0 import Memory +from mem0 import MemoryClient -agentops.init(api_key=AGENTOPS_API_KEY) -memory = Memory.from_config(config) +# Start trace for cloud operations +agentops.start_trace("cloud_memory_sync",tags=["mem0_memoryclient_example"]) -# CREATE - Add new memory -add_result = memory.add( - "User is learning Python programming", - user_id=user_id, - metadata={"skill": "programming", "level": "beginner"} -) -memory_id = add_result.get("memory_id") +try: + # Initialize MemoryClient - AgentOps tracks API authentication + client = MemoryClient(api_key="your_mem0_api_key") -# READ - Get specific memory -memory_details = memory.get(memory_id, user_id=user_id) + # Batch add memories - AgentOps tracks bulk operations + messages = [ + {"role": "user", "content": "I work in software engineering"}, + {"role": "user", "content": "I prefer Python over Java"}, + ] -# UPDATE - Modify existing memory -update_result = memory.update( - memory_id=memory_id, - text="User is now intermediate at Python programming", - user_id=user_id -) + client.add(messages, user_id="user_123") -# DELETE - Remove specific memory -delete_result = memory.delete(memory_id=memory_id, user_id=user_id) + # Search with filters - AgentOps tracks complex queries + filters = {"AND": [{"user_id": "user_123"}]} + results = client.search( + query="What programming languages does the user know?", + filters=filters, + version="v2" + ) -# DELETE ALL - Clear all memories for user -delete_all_result = memory.delete_all(user_id=user_id) + # End trace - AgentOps aggregates all operations + agentops.end_trace(end_state="success") +except Exception as e: + agentops.end_trace(end_state="error") ``` +## What You'll See in AgentOps +When using Mem0 with AgentOps, your dashboard will show: -## AgentOps Dashboard Features - -After running your Mem0 operations, visit the [AgentOps Dashboard](https://app.agentops.ai/) to see: - -- **Memory Operations**: Detailed traces of add, search, update, and delete operations -- **Performance Metrics**: Latency and success rates for memory operations -- **User Tracking**: Memory operations organized by user_id and agent_id -- **Metadata Analysis**: Custom metadata and tags for memory categorization -- **Error Monitoring**: Failed operations with full stack traces -- **Search Analytics**: Query patterns and retrieval effectiveness +1. **Memory Operation Timeline**: Visual flow of all memory operations +2. **Search Analytics**: Query patterns and retrieval effectiveness +3. **Memory Growth**: Track how user memories accumulate over time +4. **Performance Metrics**: Latency for adds, searches, and retrievals +5. **Error Tracking**: Failed operations with full error context +6. **Cost Attribution**: Token usage for memory extraction and searches ## Examples - - - Complete example demonstrating all four Mem0 memory classes with AgentOps instrumentation. + + + Simple example showing memory storage and retrieval with AgentOps tracking + + + + Track concurrent memory operations with async/await patterns - - -## Best Practices -1. **Initialize AgentOps**: Always call `agentops.init()`. -2. **Use Metadata**: Add meaningful metadata to memories for better organization and filtering -3. **Handle Errors**: Wrap memory operations in try-catch blocks for robust error handling -4. **User Segmentation**: Use consistent user_id patterns for proper memory isolation -5. **Async Operations**: Use async classes for better performance in concurrent scenarios -6. **Memory Cleanup**: Regularly clean up old or irrelevant memories to maintain performance + - - \ No newline at end of file + \ No newline at end of file diff --git a/docs/v2/introduction.mdx b/docs/v2/introduction.mdx index 02c252328..27dc7b027 100644 --- a/docs/v2/introduction.mdx +++ b/docs/v2/introduction.mdx @@ -23,6 +23,7 @@ description: "AgentOps is the developer favorite platform for testing, debugging } iconType="image" href="/v2/integrations/litellm" /> } iconType="image" href="/v2/integrations/ibm_watsonx_ai" /> } iconType="image" href="/v2/integrations/xai" /> + } iconType="image" href="/v2/integrations/mem0" /> ### Agent Frameworks diff --git a/examples/mem0/mem0_memory_example.ipynb b/examples/mem0/mem0_memory_example.ipynb new file mode 100644 index 000000000..5c21180bc --- /dev/null +++ b/examples/mem0/mem0_memory_example.ipynb @@ -0,0 +1,322 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4695c5e8", + "metadata": {}, + "source": [ + "\n", + "# Memory Operations with Mem0\n", + "\n", + "This example demonstrates how to use Mem0's memory management capabilities with both synchronous and asynchronous operations to store, search, and manage conversational context and user preferences.\n", + "\n", + "## Overview\n", + "\n", + "This example showcases practical memory management operations where we:\n", + "\n", + "1. **Initialize Mem0 Memory instances** for both sync and async operations\n", + "2. **Store conversation history** and user preferences with metadata\n", + "3. **Search memories** using natural language queries\n", + "4. **Compare performance** between synchronous and asynchronous memory operations\n", + "\n", + "By using async operations, you can perform multiple memory operations simultaneously instead of waiting for each one to complete sequentially. This is particularly beneficial when dealing with multiple memory additions or searches.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "443ab37e", + "metadata": {}, + "outputs": [], + "source": [ + "# Install the required dependencies:\n", + "%pip install agentops\n", + "%pip install mem0ai\n", + "%pip install python-dotenv" + ] + }, + { + "cell_type": "markdown", + "id": "994a6771", + "metadata": {}, + "source": [ + "## Setup and Imports\n", + "\n", + "Import the required libraries for local memory management with Mem0. We'll use both Memory and AsyncMemory classes to demonstrate different execution patterns for memory operations.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "id": "dbc4d41d", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from mem0 import Memory, AsyncMemory\n", + "import os\n", + "import asyncio\n", + "import logging\n", + "from dotenv import load_dotenv\n", + "import agentops" + ] + }, + { + "cell_type": "markdown", + "id": "970fc737", + "metadata": {}, + "source": [ + "## Environment Configuration\n", + "\n", + "Set up environment variables for API keys. These are essential for authenticating with AgentOps for tracing and OpenAI for the language model used in memory operations.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "502e4b22", + "metadata": { + "lines_to_next_cell": 1 + }, + "outputs": [], + "source": [ + "os.environ[\"AGENTOPS_API_KEY\"] = os.getenv(\"AGENTOPS_API_KEY\")\n", + "os.environ[\"OPENAI_API_KEY\"] = os.getenv(\"OPENAI_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "91bb29c1", + "metadata": {}, + "source": [ + "## Configuration and Sample Data\n", + "\n", + "Set up the configuration for local memory storage and define sample user data. The configuration specifies the LLM provider and model settings for processing memories.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "id": "6a99b32f", + "metadata": {}, + "outputs": [], + "source": [ + "local_config = {\n", + " \"llm\": {\n", + " \"provider\": \"openai\",\n", + " \"config\": {\n", + " \"model\": \"gpt-4o-mini\",\n", + " \"temperature\": 0.1,\n", + " \"max_tokens\": 2000,\n", + " },\n", + " }\n", + "}\n", + "user_id = \"alice_demo\"\n", + "agent_id = \"assistant_demo\"\n", + "run_id = \"session_001\"\n", + "\n", + "sample_messages = [\n", + " {\"role\": \"user\", \"content\": \"I'm planning to watch a movie tonight. Any recommendations?\"},\n", + " {\"role\": \"assistant\", \"content\": \"How about a thriller? They can be quite engaging.\"},\n", + " {\"role\": \"user\", \"content\": \"I'm not a big fan of thriller movies but I love sci-fi movies.\"},\n", + " {\n", + " \"role\": \"assistant\",\n", + " \"content\": \"Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future.\",\n", + " },\n", + "]\n", + "\n", + "sample_preferences = [\n", + " \"I prefer dark roast coffee over light roast\",\n", + " \"I exercise every morning at 6 AM\",\n", + " \"I'm vegetarian and avoid all meat products\",\n", + " \"I love reading science fiction novels\",\n", + " \"I work in software engineering\",\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "2def0966", + "metadata": {}, + "source": [ + "## Synchronous Memory Operations\n", + "\n", + "This function demonstrates sequential memory operations using the synchronous Memory class. While straightforward to implement, each operation must complete before the next begins, which can impact performance.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "b863e6ed", + "metadata": {}, + "outputs": [], + "source": [ + "def demonstrate_sync_memory(local_config, sample_messages, sample_preferences, user_id):\n", + " \"\"\"\n", + " Demonstrate synchronous Memory class operations.\n", + " \"\"\"\n", + "\n", + " agentops.start_trace(\"mem0_memory_example\", tags=[\"mem0_memory_example\"])\n", + " try:\n", + " \n", + " memory = Memory.from_config(local_config)\n", + "\n", + " result = memory.add(\n", + " sample_messages, user_id=user_id, metadata={\"category\": \"movie_preferences\", \"session\": \"demo\"}\n", + " )\n", + "\n", + " for i, preference in enumerate(sample_preferences):\n", + " result = memory.add(preference, user_id=user_id, metadata={\"type\": \"preference\", \"index\": i})\n", + " \n", + " search_queries = [\n", + " \"What movies does the user like?\",\n", + " \"What are the user's food preferences?\",\n", + " \"When does the user exercise?\",\n", + " ]\n", + "\n", + " for query in search_queries:\n", + " results = memory.search(query, user_id=user_id)\n", + " \n", + " if results and \"results\" in results:\n", + " for j, result in enumerate(results): \n", + " print(f\"Result {j+1}: {result.get('memory', 'N/A')}\")\n", + " else:\n", + " print(\"No results found\")\n", + "\n", + " all_memories = memory.get_all(user_id=user_id)\n", + " if all_memories and \"results\" in all_memories:\n", + " print(f\"Total memories: {len(all_memories['results'])}\")\n", + "\n", + " delete_all_result = memory.delete_all(user_id=user_id)\n", + " print(f\"Delete all result: {delete_all_result}\")\n", + "\n", + " agentops.end_trace(end_state=\"success\")\n", + " except Exception as e:\n", + " agentops.end_trace(end_state=\"error\")" + ] + }, + { + "cell_type": "markdown", + "id": "66965f64", + "metadata": {}, + "source": [ + "## Asynchronous Memory Operations\n", + "\n", + "This function showcases concurrent memory operations using AsyncMemory. By leveraging asyncio.gather(), multiple operations execute simultaneously, significantly reducing total execution time for I/O-bound tasks.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "id": "eae41613", + "metadata": {}, + "outputs": [], + "source": [ + "async def demonstrate_async_memory(local_config, sample_messages, sample_preferences, user_id):\n", + " \"\"\"\n", + " Demonstrate asynchronous Memory class operations with concurrent execution.\n", + " \"\"\"\n", + "\n", + " agentops.start_trace(\"mem0_memory_async_example\", tags=[\"mem0_memory_async_example\"])\n", + " try:\n", + "\n", + " async_memory = await AsyncMemory.from_config(local_config)\n", + "\n", + " result = await async_memory.add(\n", + " sample_messages, user_id=user_id, metadata={\"category\": \"async_movie_preferences\", \"session\": \"async_demo\"}\n", + " )\n", + "\n", + " async def add_preference(preference, index):\n", + " \"\"\"Helper function to add a single preference asynchronously.\"\"\"\n", + " return await async_memory.add(\n", + " preference, user_id=user_id, metadata={\"type\": \"async_preference\", \"index\": index}\n", + " )\n", + "\n", + " tasks = [add_preference(pref, i) for i, pref in enumerate(sample_preferences)]\n", + " results = await asyncio.gather(*tasks)\n", + " for i, result in enumerate(results):\n", + " print(f\"Added async preference {i+1}: {result}\")\n", + "\n", + " search_queries = [\n", + " \"What movies does the user like?\",\n", + " \"What are the user's dietary restrictions?\",\n", + " \"What does the user do for work?\",\n", + " ]\n", + "\n", + " async def search_memory(query):\n", + " \"\"\"Helper function to perform async memory search.\"\"\"\n", + " return await async_memory.search(query, user_id=user_id), query\n", + "\n", + " search_tasks = [search_memory(query) for query in search_queries]\n", + " search_results = await asyncio.gather(*search_tasks)\n", + "\n", + " for result, query in search_results:\n", + " if result and \"results\" in result:\n", + " for j, res in enumerate(result[\"results\"]):\n", + " print(f\"Result {j+1}: {res.get('memory', 'N/A')}\")\n", + " else:\n", + " print(\"No results found\")\n", + "\n", + " all_memories = await async_memory.get_all(user_id=user_id)\n", + " if all_memories and \"results\" in all_memories:\n", + " print(f\"Total async memories: {len(all_memories['results'])}\")\n", + "\n", + " delete_all_result = await async_memory.delete_all(user_id=user_id)\n", + " print(f\"Delete all result: {delete_all_result}\")\n", + "\n", + " agentops.end_trace(end_state=\"success\")\n", + "\n", + " except Exception as e:\n", + " agentops.end_trace(end_state=\"error\")" + ] + }, + { + "cell_type": "markdown", + "id": "9689055c", + "metadata": {}, + "source": [ + "## Execute Demonstrations\n", + "\n", + "Run both synchronous and asynchronous demonstrations to compare their execution patterns and performance. The async version demonstrates the benefits of concurrent execution for multiple memory operations.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "714436f5", + "metadata": {}, + "outputs": [], + "source": [ + "# Execute both sync and async demonstrations\n", + "demonstrate_sync_memory(local_config, sample_messages, sample_preferences, user_id)\n", + "await demonstrate_async_memory(local_config, sample_messages, sample_preferences, user_id)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + }, + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/mem0/mem0_memory_example.py b/examples/mem0/mem0_memory_example.py new file mode 100644 index 000000000..b891111a4 --- /dev/null +++ b/examples/mem0/mem0_memory_example.py @@ -0,0 +1,225 @@ +""" +# Memory Operations with Mem0 + +This example demonstrates how to use Mem0's memory management capabilities with both synchronous and asynchronous operations to store, search, and manage conversational context and user preferences. + +## Overview + +This example showcases practical memory management operations where we: + +1. **Initialize Mem0 Memory instances** for both sync and async operations +2. **Store conversation history** and user preferences with metadata +3. **Search memories** using natural language queries +4. **Compare performance** between synchronous and asynchronous memory operations + +By using async operations, you can perform multiple memory operations simultaneously instead of waiting for each one to complete sequentially. This is particularly beneficial when dealing with multiple memory additions or searches. +""" +import os +import asyncio +import logging +from dotenv import load_dotenv + +# Load environment variables first +load_dotenv() + +# Enable debug logging for AgentOps +os.environ["AGENTOPS_LOG_LEVEL"] = "DEBUG" + +# Set environment variables before importing +os.environ["AGENTOPS_API_KEY"] = os.getenv("AGENTOPS_API_KEY") +os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY") + +# Now import mem0 - it will be instrumented by agentops +from mem0 import Memory, AsyncMemory + +# Import agentops BEFORE mem0 to ensure proper instrumentation +import agentops + + +def demonstrate_sync_memory(local_config, sample_messages, sample_preferences, user_id): + """ + Demonstrate synchronous Memory class operations. + + This function performs sequential memory operations including: + - Adding conversation messages with metadata + - Storing individual user preferences + - Searching memories using natural language queries + - Retrieving all memories for a user + - Cleaning up memories after demonstration + + Args: + local_config: Configuration dict for Memory initialization + sample_messages: List of conversation messages to store + sample_preferences: List of user preferences to store + user_id: Unique identifier for the user + + Performance note: Sequential operations take longer as each operation + must complete before the next one begins. + """ + + agentops.start_trace("mem0_memory_example", tags=["mem0_memory_example"]) + try: + + # Initialize sync Memory with local configuration + memory = Memory.from_config(local_config) + + # Add conversation messages with metadata for categorization + result = memory.add( + sample_messages, user_id=user_id, metadata={"category": "movie_preferences", "session": "demo"} + ) + + # Add individual preferences sequentially + for i, preference in enumerate(sample_preferences): + result = memory.add(preference, user_id=user_id, metadata={"type": "preference", "index": i}) + + # 2. SEARCH operations - demonstrate natural language search capabilities + search_queries = [ + "What movies does the user like?", + "What are the user's food preferences?", + "When does the user exercise?", + ] + + for query in search_queries: + results = memory.search(query, user_id=user_id) + + if results and "results" in results: + for j, result in enumerate(results["results"][:2]): # Show top 2 + print(f"Result {j+1}: {result.get('memory', 'N/A')}") + else: + print("No results found") + + # 3. GET_ALL operations - retrieve all memories for the user + all_memories = memory.get_all(user_id=user_id) + if all_memories and "results" in all_memories: + print(f"Total memories: {len(all_memories['results'])}") + + # Cleanup - remove all memories for the user + delete_all_result = memory.delete_all(user_id=user_id) + print(f"Delete all result: {delete_all_result}") + + agentops.end_trace(end_state="success") + except Exception as e: + agentops.end_trace(end_state="error") + + +async def demonstrate_async_memory(local_config, sample_messages, sample_preferences, user_id): + """ + Demonstrate asynchronous Memory class operations with concurrent execution. + + This function performs concurrent memory operations including: + - Adding conversation messages asynchronously + - Storing multiple preferences concurrently using asyncio.gather() + - Performing parallel search operations + - Retrieving all memories asynchronously + - Cleaning up memories after demonstration + + Args: + local_config: Configuration dict for AsyncMemory initialization + sample_messages: List of conversation messages to store + sample_preferences: List of user preferences to store + user_id: Unique identifier for the user + + Performance benefit: Concurrent operations significantly reduce total execution time + by running multiple memory operations in parallel. + """ + + agentops.start_trace("mem0_memory_async_example") + try: + # Initialize async Memory with configuration + async_memory = await AsyncMemory.from_config(local_config) + + # 1. ADD operation - store conversation with async context + # Add conversation messages + result = await async_memory.add( + sample_messages, user_id=user_id, metadata={"category": "async_movie_preferences", "session": "async_demo"} + ) + + + # Add preferences concurrently using asyncio.gather() + async def add_preference(preference, index): + """Helper function to add a single preference asynchronously.""" + return await async_memory.add( + preference, user_id=user_id, metadata={"type": "async_preference", "index": index} + ) + + # Create tasks for concurrent execution + tasks = [add_preference(pref, i) for i, pref in enumerate(sample_preferences)] + results = await asyncio.gather(*tasks) + for i, result in enumerate(results): + print(f"Added async preference {i+1}: {result}") + + # 2. SEARCH operations - perform multiple searches concurrently + search_queries = [ + "What movies does the user like?", + "What are the user's dietary restrictions?", + "What does the user do for work?", + ] + + async def search_memory(query): + """Helper function to perform async memory search.""" + return await async_memory.search(query, user_id=user_id), query + + # Execute all searches concurrently + search_tasks = [search_memory(query) for query in search_queries] + search_results = await asyncio.gather(*search_tasks) + + for result, query in search_results: + if result and "results" in result: + for j, res in enumerate(result["results"][:2]): + print(f"Result {j+1}: {res.get('memory', 'N/A')}") + else: + print("No results found") + + # 3. GET_ALL operations - retrieve all memories asynchronously + all_memories = await async_memory.get_all(user_id=user_id) + if all_memories and "results" in all_memories: + print(f"Total async memories: {len(all_memories['results'])}") + + # Cleanup - remove all memories asynchronously + delete_all_result = await async_memory.delete_all(user_id=user_id) + print(f"Delete all result: {delete_all_result}") + + agentops.end_trace(end_state="success") + + except Exception as e: + agentops.end_trace(end_state="error") + +# Configuration for local memory (Memory) +# This configuration specifies the LLM provider and model settings +local_config = { + "llm": { + "provider": "openai", + "config": { + "model": "gpt-4o-mini", + "temperature": 0.1, + "max_tokens": 2000, + "api_key": os.getenv("OPENAI_API_KEY"), + }, + } +} +# Sample user data +user_id = "alice_demo" +agent_id = "assistant_demo" +run_id = "session_001" + +# Sample conversation data demonstrating movie preference discovery +sample_messages = [ + {"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"}, + {"role": "assistant", "content": "How about a thriller? They can be quite engaging."}, + {"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."}, + { + "role": "assistant", + "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future.", + }, +] + +# Sample user preferences covering various aspects of daily life +sample_preferences = [ + "I prefer dark roast coffee over light roast", + "I exercise every morning at 6 AM", +] + + +# Execute both sync and async demonstrations +demonstrate_sync_memory(local_config, sample_messages, sample_preferences, user_id) +asyncio.run(demonstrate_async_memory(local_config, sample_messages, sample_preferences, user_id)) \ No newline at end of file diff --git a/examples/mem0/mem0_memoryclient_example.ipynb b/examples/mem0/mem0_memoryclient_example.ipynb new file mode 100644 index 000000000..cbcb72496 --- /dev/null +++ b/examples/mem0/mem0_memoryclient_example.ipynb @@ -0,0 +1,554 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ecd1d2ee", + "metadata": {}, + "source": [ + "\n", + "# Cloud Memory Operations with Mem0 MemoryClient\n", + "\n", + "This example demonstrates how to use Mem0's cloud-based MemoryClient for managing conversational memory and user preferences with both synchronous and asynchronous operations.\n", + "\n", + "## Overview\n", + "\n", + "This example showcases cloud-based memory management operations where we:\n", + "\n", + "1. **Initialize MemoryClient instances** for both sync and async cloud operations\n", + "2. **Store conversation history** in the cloud with rich metadata\n", + "3. **Perform concurrent operations** using async/await patterns\n", + "4. **Search and filter memories** using natural language and structured queries\n", + "\n", + "By using the cloud-based MemoryClient with async operations, you can leverage Mem0's managed infrastructure while performing multiple memory operations simultaneously. This is ideal for production applications that need scalable memory management without managing local storage.\n" + ] + }, + { + "cell_type": "markdown", + "id": "a63847c4", + "metadata": {}, + "source": [ + "## Setup and Imports\n", + "\n", + "First, we'll import the necessary libraries for working with Mem0's cloud-based memory management system. We'll use both synchronous and asynchronous clients to demonstrate different usage patterns.\n" + ] + }, + { + "cell_type": "markdown", + "id": "f98af04f", + "metadata": {}, + "source": [ + "# Install the required dependencies:\n", + "%pip install agentops\n", + "%pip install mem0ai\n", + "%pip install python-dotenv" + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "id": "69b834f6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: agentops in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (0.4.14)\n", + "Requirement already satisfied: httpx<0.29.0,>=0.24.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from agentops) (0.28.1)\n", + "Requirement already satisfied: opentelemetry-api>1.29.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from agentops) (1.34.1)\n", + "Requirement already satisfied: opentelemetry-exporter-otlp-proto-http>1.29.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from agentops) (1.34.1)\n", + "Requirement already satisfied: opentelemetry-instrumentation>=0.50b0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from agentops) (0.55b1)\n", + "Requirement already satisfied: opentelemetry-sdk>1.29.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from agentops) (1.34.1)\n", + "Requirement already satisfied: opentelemetry-semantic-conventions>=0.50b0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from agentops) (0.55b1)\n", + "Requirement already satisfied: ordered-set<5.0.0,>=4.0.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from agentops) (4.1.0)\n", + "Requirement already satisfied: packaging<25.0,>=21.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from agentops) (24.2)\n", + "Requirement already satisfied: psutil<7.0.1,>=5.9.8 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from agentops) (7.0.0)\n", + "Requirement already satisfied: pyyaml<7.0,>=5.3 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from agentops) (6.0.2)\n", + "Requirement already satisfied: requests<3.0.0,>=2.0.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from agentops) (2.32.4)\n", + "Requirement already satisfied: termcolor<2.5.0,>=2.3.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from agentops) (2.4.0)\n", + "Requirement already satisfied: wrapt<2.0.0,>=1.0.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from agentops) (1.17.2)\n", + "Requirement already satisfied: anyio in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from httpx<0.29.0,>=0.24.0->agentops) (4.9.0)\n", + "Requirement already satisfied: certifi in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from httpx<0.29.0,>=0.24.0->agentops) (2025.4.26)\n", + "Requirement already satisfied: httpcore==1.* in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from httpx<0.29.0,>=0.24.0->agentops) (1.0.9)\n", + "Requirement already satisfied: idna in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from httpx<0.29.0,>=0.24.0->agentops) (3.10)\n", + "Requirement already satisfied: h11>=0.16 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from httpcore==1.*->httpx<0.29.0,>=0.24.0->agentops) (0.16.0)\n", + "Requirement already satisfied: charset_normalizer<4,>=2 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from requests<3.0.0,>=2.0.0->agentops) (3.4.2)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from requests<3.0.0,>=2.0.0->agentops) (2.4.0)\n", + "Requirement already satisfied: importlib-metadata<8.8.0,>=6.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from opentelemetry-api>1.29.0->agentops) (8.7.0)\n", + "Requirement already satisfied: typing-extensions>=4.5.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from opentelemetry-api>1.29.0->agentops) (4.14.0)\n", + "Requirement already satisfied: zipp>=3.20 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from importlib-metadata<8.8.0,>=6.0->opentelemetry-api>1.29.0->agentops) (3.23.0)\n", + "Requirement already satisfied: googleapis-common-protos~=1.52 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from opentelemetry-exporter-otlp-proto-http>1.29.0->agentops) (1.70.0)\n", + "Requirement already satisfied: opentelemetry-exporter-otlp-proto-common==1.34.1 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from opentelemetry-exporter-otlp-proto-http>1.29.0->agentops) (1.34.1)\n", + "Requirement already satisfied: opentelemetry-proto==1.34.1 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from opentelemetry-exporter-otlp-proto-http>1.29.0->agentops) (1.34.1)\n", + "Requirement already satisfied: protobuf<6.0,>=5.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from opentelemetry-proto==1.34.1->opentelemetry-exporter-otlp-proto-http>1.29.0->agentops) (5.29.5)\n", + "Requirement already satisfied: sniffio>=1.1 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from anyio->httpx<0.29.0,>=0.24.0->agentops) (1.3.1)\n", + "Note: you may need to restart the kernel to use updated packages.\n", + "Requirement already satisfied: mem0ai in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (0.1.104)\n", + "Requirement already satisfied: openai>=1.33.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from mem0ai) (1.75.0)\n", + "Requirement already satisfied: posthog>=3.5.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from mem0ai) (3.25.0)\n", + "Requirement already satisfied: pydantic>=2.7.3 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from mem0ai) (2.11.4)\n", + "Requirement already satisfied: pytz>=2024.1 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from mem0ai) (2024.2)\n", + "Requirement already satisfied: qdrant-client>=1.9.1 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from mem0ai) (1.14.2)\n", + "Requirement already satisfied: sqlalchemy>=2.0.31 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from mem0ai) (2.0.41)\n", + "Requirement already satisfied: anyio<5,>=3.5.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from openai>=1.33.0->mem0ai) (4.9.0)\n", + "Requirement already satisfied: distro<2,>=1.7.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from openai>=1.33.0->mem0ai) (1.9.0)\n", + "Requirement already satisfied: httpx<1,>=0.23.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from openai>=1.33.0->mem0ai) (0.28.1)\n", + "Requirement already satisfied: jiter<1,>=0.4.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from openai>=1.33.0->mem0ai) (0.8.2)\n", + "Requirement already satisfied: sniffio in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from openai>=1.33.0->mem0ai) (1.3.1)\n", + "Requirement already satisfied: tqdm>4 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from openai>=1.33.0->mem0ai) (4.67.1)\n", + "Requirement already satisfied: typing-extensions<5,>=4.11 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from openai>=1.33.0->mem0ai) (4.14.0)\n", + "Requirement already satisfied: idna>=2.8 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from anyio<5,>=3.5.0->openai>=1.33.0->mem0ai) (3.10)\n", + "Requirement already satisfied: certifi in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from httpx<1,>=0.23.0->openai>=1.33.0->mem0ai) (2025.4.26)\n", + "Requirement already satisfied: httpcore==1.* in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from httpx<1,>=0.23.0->openai>=1.33.0->mem0ai) (1.0.9)\n", + "Requirement already satisfied: h11>=0.16 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from httpcore==1.*->httpx<1,>=0.23.0->openai>=1.33.0->mem0ai) (0.16.0)\n", + "Requirement already satisfied: annotated-types>=0.6.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from pydantic>=2.7.3->mem0ai) (0.7.0)\n", + "Requirement already satisfied: pydantic-core==2.33.2 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from pydantic>=2.7.3->mem0ai) (2.33.2)\n", + "Requirement already satisfied: typing-inspection>=0.4.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from pydantic>=2.7.3->mem0ai) (0.4.0)\n", + "Requirement already satisfied: requests<3.0,>=2.7 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from posthog>=3.5.0->mem0ai) (2.32.4)\n", + "Requirement already satisfied: six>=1.5 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from posthog>=3.5.0->mem0ai) (1.17.0)\n", + "Requirement already satisfied: monotonic>=1.5 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from posthog>=3.5.0->mem0ai) (1.6)\n", + "Requirement already satisfied: backoff>=1.10.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from posthog>=3.5.0->mem0ai) (2.2.1)\n", + "Requirement already satisfied: python-dateutil>2.1 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from posthog>=3.5.0->mem0ai) (2.9.0.post0)\n", + "Requirement already satisfied: charset_normalizer<4,>=2 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from requests<3.0,>=2.7->posthog>=3.5.0->mem0ai) (3.4.2)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from requests<3.0,>=2.7->posthog>=3.5.0->mem0ai) (2.4.0)\n", + "Requirement already satisfied: grpcio>=1.41.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from qdrant-client>=1.9.1->mem0ai) (1.71.0)\n", + "Requirement already satisfied: numpy>=1.21 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from qdrant-client>=1.9.1->mem0ai) (2.2.5)\n", + "Requirement already satisfied: portalocker<3.0.0,>=2.7.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from qdrant-client>=1.9.1->mem0ai) (2.10.1)\n", + "Requirement already satisfied: protobuf>=3.20.0 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from qdrant-client>=1.9.1->mem0ai) (5.29.5)\n", + "Requirement already satisfied: h2<5,>=3 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from httpx[http2]>=0.20.0->qdrant-client>=1.9.1->mem0ai) (4.2.0)\n", + "Requirement already satisfied: hyperframe<7,>=6.1 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from h2<5,>=3->httpx[http2]>=0.20.0->qdrant-client>=1.9.1->mem0ai) (6.1.0)\n", + "Requirement already satisfied: hpack<5,>=4.1 in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (from h2<5,>=3->httpx[http2]>=0.20.0->qdrant-client>=1.9.1->mem0ai) (4.1.0)\n", + "Note: you may need to restart the kernel to use updated packages.\n", + "Requirement already satisfied: python-dotenv in /Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages (1.1.0)\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# Install the required dependencies:\n", + "%pip install agentops\n", + "%pip install mem0ai\n", + "%pip install python-dotenv" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "id": "e552e158", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from mem0 import MemoryClient, AsyncMemoryClient\n", + "import agentops\n", + "import os\n", + "import asyncio\n", + "from dotenv import load_dotenv" + ] + }, + { + "cell_type": "markdown", + "id": "a70fadde", + "metadata": {}, + "source": [ + "## Environment Configuration\n", + "\n", + "Load environment variables including API keys for AgentOps, Mem0, and OpenAI. These credentials are required for authenticating with the respective services.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 97, + "id": "969f7c42", + "metadata": {}, + "outputs": [], + "source": [ + "# Load environment variables\n", + "load_dotenv()\n", + "os.environ[\"AGENTOPS_API_KEY\"] = os.getenv(\"AGENTOPS_API_KEY\")\n", + "mem0_api_key = os.getenv(\"MEM0_API_KEY\")\n", + "os.environ[\"OPENAI_API_KEY\"] = os.getenv(\"OPENAI_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "3f630d40", + "metadata": {}, + "source": [ + "## Sample Data Setup\n", + "\n", + "Define user identifiers and sample data that will be used throughout the demonstration. This includes user IDs for tracking memory ownership and agent/session identifiers for context.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "id": "0abf3fe7", + "metadata": {}, + "outputs": [], + "source": [ + "# Sample user data for demonstration\n", + "user_id = \"alice_demo\"\n", + "agent_id = \"assistant_demo\"\n", + "run_id = \"session_001\"\n", + "\n", + "# Sample conversation data demonstrating preference discovery through dialogue\n", + "sample_messages = [\n", + " {\"role\": \"user\", \"content\": \"I'm planning to watch a movie tonight. Any recommendations?\"},\n", + " {\"role\": \"assistant\", \"content\": \"How about a thriller? They can be quite engaging.\"},\n", + " {\"role\": \"user\", \"content\": \"I'm not a big fan of thriller movies but I love sci-fi movies.\"},\n", + " {\n", + " \"role\": \"assistant\",\n", + " \"content\": \"Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future.\",\n", + " },\n", + "]\n", + "\n", + "# Sample user preferences representing various personal attributes\n", + "sample_preferences = [\n", + " \"I prefer dark roast coffee over light roast\",\n", + " \"I exercise every morning at 6 AM\",\n", + " \"I'm vegetarian and avoid all meat products\",\n", + " \"I love reading science fiction novels\",\n", + " \"I work in software engineering\",\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "046d2588", + "metadata": {}, + "source": [ + "## Synchronous Memory Operations\n", + "\n", + "The following function demonstrates how to use the synchronous MemoryClient for sequential cloud memory operations. This approach is straightforward but operations execute one after another.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "id": "63d2f851", + "metadata": {}, + "outputs": [], + "source": [ + "def demonstrate_sync_memory_client(sample_messages, sample_preferences, user_id):\n", + " \"\"\"\n", + " Demonstrate synchronous MemoryClient operations with cloud storage.\n", + " \n", + " This function performs sequential cloud memory operations including:\n", + " - Initializing cloud-based memory client with API authentication\n", + " - Adding conversation messages to cloud storage\n", + " - Storing user preferences with metadata\n", + " - Searching memories using natural language\n", + " - Retrieving memories with filters\n", + " - Cleaning up cloud memories\n", + " \n", + " \"\"\"\n", + " agentops.start_trace(\"mem0_memoryclient_sync_example\",tags=[\"mem0_memoryclient_example\"])\n", + " try:\n", + " # Initialize sync MemoryClient with API key for cloud access\n", + " client = MemoryClient(api_key=mem0_api_key)\n", + "\n", + "\n", + " # Add conversation to cloud storage with metadata\n", + " result = client.add(\n", + " sample_messages, user_id=user_id, metadata={\"category\": \"cloud_movie_preferences\", \"session\": \"cloud_demo\"},version=\"v2\"\n", + " )\n", + " print(f\"Add result: {result}\")\n", + "\n", + " # Add preferences sequentially to cloud\n", + " for i, preference in enumerate(sample_preferences[:3]): # Limit for demo\n", + " # Convert string preference to message format\n", + " preference_message = [{\"role\": \"user\", \"content\": preference}]\n", + " result = client.add(preference_message, user_id=user_id, metadata={\"type\": \"cloud_preference\", \"index\": i})\n", + " \n", + "\n", + " # 2. SEARCH operations - leverage cloud search capabilities\n", + " search_result = client.search(\"What are the user's movie preferences?\", user_id=user_id)\n", + " print(f\"Search result: {search_result}\")\n", + "\n", + " # 3. GET_ALL - retrieve all memories for the user\n", + " all_memories = client.get_all(user_id=user_id, limit=10)\n", + " print(f\"Cloud memories retrieved: {all_memories}\")\n", + "\n", + " # Cleanup - remove all user memories from cloud\n", + " delete_all_result = client.delete_all(user_id=user_id)\n", + " print(f\"Delete all result: {delete_all_result}\")\n", + " agentops.end_trace(end_state=\"success\")\n", + " except Exception as e:\n", + " agentops.end_trace(end_state=\"error\")" + ] + }, + { + "cell_type": "markdown", + "id": "6e629329", + "metadata": {}, + "source": [ + "## Asynchronous Memory Operations\n", + "\n", + "This function showcases the power of asynchronous operations with Mem0's AsyncMemoryClient. By using async/await patterns, we can execute multiple memory operations concurrently, significantly improving performance.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 100, + "id": "2462a05f", + "metadata": {}, + "outputs": [], + "source": [ + "async def demonstrate_async_memory_client(sample_messages, sample_preferences, user_id):\n", + " \"\"\"\n", + " Demonstrate asynchronous MemoryClient operations with concurrent cloud access.\n", + " \n", + " This function performs concurrent cloud memory operations including:\n", + " - Initializing async cloud-based memory client\n", + " - Adding multiple memories concurrently using asyncio.gather()\n", + " - Performing parallel search operations across cloud storage\n", + " - Retrieving filtered memories asynchronously\n", + " - Cleaning up cloud memories efficiently\n", + " \n", + " \"\"\"\n", + " agentops.start_trace(\"mem0_memoryclient_async_example\",tags=[\"mem0_memoryclient_example\"])\n", + " try:\n", + " # Initialize async MemoryClient for concurrent cloud operations\n", + " async_client = AsyncMemoryClient(api_key=mem0_api_key)\n", + " \n", + " # Add conversation and preferences concurrently to cloud\n", + " add_conversation_task = async_client.add(\n", + " sample_messages, user_id=user_id, metadata={\"category\": \"async_cloud_movies\", \"session\": \"async_cloud_demo\"}\n", + " )\n", + "\n", + " # Create tasks for adding preferences in parallel\n", + " add_preference_tasks = [\n", + " async_client.add([{\"role\": \"user\", \"content\": pref}], user_id=user_id, metadata={\"type\": \"async_cloud_preference\", \"index\": i})\n", + " for i, pref in enumerate(sample_preferences[:3])\n", + " ]\n", + "\n", + " # Execute all add operations concurrently\n", + " results = await asyncio.gather(add_conversation_task, *add_preference_tasks)\n", + " for i, result in enumerate(results):\n", + " print(f\"{i+1}. {result}\")\n", + "\n", + " # 2. Concurrent SEARCH operations - multiple cloud searches in parallel\n", + " search_tasks = [\n", + " async_client.search(\"movie preferences\", user_id=user_id),\n", + " async_client.search(\"food preferences\", user_id=user_id),\n", + " async_client.search(\"work information\", user_id=user_id),\n", + " ]\n", + "\n", + " # Execute all searches concurrently\n", + " search_results = await asyncio.gather(*search_tasks)\n", + " for i, result in enumerate(search_results):\n", + " print(f\"Search {i+1} result: {result}\")\n", + "\n", + " # 3. GET_ALL operation - retrieve all memories from cloud\n", + " all_memories = await async_client.get_all(user_id=user_id, limit=10)\n", + " print(f\"Async cloud memories: {all_memories}\")\n", + "\n", + " # Final cleanup - remove all memories asynchronously\n", + " delete_all_result = await async_client.delete_all(user_id=user_id)\n", + " print(f\"Delete all result: {delete_all_result}\")\n", + "\n", + " agentops.end_trace(end_state=\"success\")\n", + "\n", + " except Exception as e:\n", + " agentops.end_trace(end_state=\"error\")" + ] + }, + { + "cell_type": "markdown", + "id": "c5e1af75", + "metadata": {}, + "source": [ + "## Execute Demonstrations\n", + "\n", + "Run both synchronous and asynchronous demonstrations to compare their behavior and performance. The async version typically completes faster due to concurrent operations, especially when dealing with multiple API calls.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 101, + "id": "3a6524b2", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "๐Ÿ–‡ AgentOps: \u001b[34m\u001b[34mSession Replay for mem0_memoryclient_sync_example trace: https://app.agentops.ai/sessions?trace_id=75c9c672ca2f2205d1bdb9b4f615ba2e\u001b[0m\u001b[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "args: ([{'role': 'user', 'content': \"I'm planning to watch a movie tonight. Any recommendations?\"}, {'role': 'assistant', 'content': 'How about a thriller? They can be quite engaging.'}, {'role': 'user', 'content': \"I'm not a big fan of thriller movies but I love sci-fi movies.\"}, {'role': 'assistant', 'content': \"Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future.\"}],)\n", + "kwargs: {'user_id': 'alice_demo', 'metadata': {'category': 'cloud_movie_preferences', 'session': 'cloud_demo'}, 'version': 'v2'}\n", + "return_value: None\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/fenil/Documents/agentops/venv/lib/python3.11/site-packages/mem0/client/main.py:34: DeprecationWarning: output_format='v1.0' is deprecated therefore setting it to 'v1.1' by default.Check out the docs for more information: https://docs.mem0.ai/platform/quickstart#4-1-create-memories\n", + " return func(*args, **kwargs)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "args: None\n", + "kwargs: None\n", + "return_value: {'results': [{'id': '15b4579e-a074-4ef6-a318-03078a18858b', 'event': 'ADD', 'memory': 'Prefers sci-fi movies over thriller movies'}]}\n", + "Add result: {'results': [{'id': '15b4579e-a074-4ef6-a318-03078a18858b', 'event': 'ADD', 'memory': 'Prefers sci-fi movies over thriller movies'}]}\n", + "args: ([{'role': 'user', 'content': 'I prefer dark roast coffee over light roast'}],)\n", + "kwargs: {'user_id': 'alice_demo', 'metadata': {'type': 'cloud_preference', 'index': 0}}\n", + "return_value: None\n", + "args: None\n", + "kwargs: None\n", + "return_value: {'results': [{'id': 'd012318e-6dd3-4456-9ddb-1e33b5973201', 'event': 'ADD', 'memory': 'Prefers dark roast coffee over light roast'}]}\n", + "args: ([{'role': 'user', 'content': 'I exercise every morning at 6 AM'}],)\n", + "kwargs: {'user_id': 'alice_demo', 'metadata': {'type': 'cloud_preference', 'index': 1}}\n", + "return_value: None\n", + "args: None\n", + "kwargs: None\n", + "return_value: {'results': [{'id': '1e0829f9-ada4-4fde-8371-9a0b24410692', 'event': 'ADD', 'memory': 'Exercises every morning at 6 AM'}]}\n", + "args: ([{'role': 'user', 'content': \"I'm vegetarian and avoid all meat products\"}],)\n", + "kwargs: {'user_id': 'alice_demo', 'metadata': {'type': 'cloud_preference', 'index': 2}}\n", + "return_value: None\n", + "args: None\n", + "kwargs: None\n", + "return_value: {'results': [{'id': 'ea933b7a-ad62-4340-9e4b-422ba89ccb13', 'event': 'ADD', 'memory': 'Is vegetarian and avoids all meat products'}]}\n", + "get_search_attributes args: (\"What are the user's movie preferences?\",)\n", + "get_search_attributes kwargs: {'user_id': 'alice_demo'}\n", + "get_search_attributes return_value: None\n", + "get_search_attributes args: None\n", + "get_search_attributes kwargs: None\n", + "get_search_attributes return_value: [{'id': '15b4579e-a074-4ef6-a318-03078a18858b', 'memory': 'Prefers sci-fi movies over thriller movies', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'session': 'cloud_demo', 'category': 'cloud_movie_preferences'}, 'categories': ['user_preferences', 'entertainment'], 'created_at': '2025-06-13T07:01:02.776810-07:00', 'updated_at': '2025-06-13T07:01:02.795856-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.5759006142616313}, {'id': 'd012318e-6dd3-4456-9ddb-1e33b5973201', 'memory': 'Prefers dark roast coffee over light roast', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'cloud_preference', 'index': 0}, 'categories': ['user_preferences', 'food'], 'created_at': '2025-06-13T07:01:09.645061-07:00', 'updated_at': '2025-06-13T07:01:09.666647-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.4206016754386812}, {'id': 'ea933b7a-ad62-4340-9e4b-422ba89ccb13', 'memory': 'Is vegetarian and avoids all meat products', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'cloud_preference', 'index': 2}, 'categories': ['user_preferences', 'food'], 'created_at': '2025-06-13T07:01:24.877544-07:00', 'updated_at': '2025-06-13T07:01:24.896089-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.3932421036670084}, {'id': '1e0829f9-ada4-4fde-8371-9a0b24410692', 'memory': 'Exercises every morning at 6 AM', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'cloud_preference', 'index': 1}, 'categories': ['health'], 'created_at': '2025-06-13T07:01:16.659112-07:00', 'updated_at': '2025-06-13T07:01:16.740428-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.3198329210281394}]\n", + "Search result: [{'id': '15b4579e-a074-4ef6-a318-03078a18858b', 'memory': 'Prefers sci-fi movies over thriller movies', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'session': 'cloud_demo', 'category': 'cloud_movie_preferences'}, 'categories': ['user_preferences', 'entertainment'], 'created_at': '2025-06-13T07:01:02.776810-07:00', 'updated_at': '2025-06-13T07:01:02.795856-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.5759006142616313}, {'id': 'd012318e-6dd3-4456-9ddb-1e33b5973201', 'memory': 'Prefers dark roast coffee over light roast', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'cloud_preference', 'index': 0}, 'categories': ['user_preferences', 'food'], 'created_at': '2025-06-13T07:01:09.645061-07:00', 'updated_at': '2025-06-13T07:01:09.666647-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.4206016754386812}, {'id': 'ea933b7a-ad62-4340-9e4b-422ba89ccb13', 'memory': 'Is vegetarian and avoids all meat products', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'cloud_preference', 'index': 2}, 'categories': ['user_preferences', 'food'], 'created_at': '2025-06-13T07:01:24.877544-07:00', 'updated_at': '2025-06-13T07:01:24.896089-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.3932421036670084}, {'id': '1e0829f9-ada4-4fde-8371-9a0b24410692', 'memory': 'Exercises every morning at 6 AM', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'cloud_preference', 'index': 1}, 'categories': ['health'], 'created_at': '2025-06-13T07:01:16.659112-07:00', 'updated_at': '2025-06-13T07:01:16.740428-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.3198329210281394}]\n", + "Cloud memories retrieved: [{'id': 'ea933b7a-ad62-4340-9e4b-422ba89ccb13', 'memory': 'Is vegetarian and avoids all meat products', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'cloud_preference', 'index': 2}, 'categories': ['user_preferences', 'food'], 'created_at': '2025-06-13T07:01:24.877544-07:00', 'updated_at': '2025-06-13T07:01:24.896089-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None}, {'id': '1e0829f9-ada4-4fde-8371-9a0b24410692', 'memory': 'Exercises every morning at 6 AM', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'cloud_preference', 'index': 1}, 'categories': ['health'], 'created_at': '2025-06-13T07:01:16.659112-07:00', 'updated_at': '2025-06-13T07:01:16.740428-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None}, {'id': 'd012318e-6dd3-4456-9ddb-1e33b5973201', 'memory': 'Prefers dark roast coffee over light roast', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'cloud_preference', 'index': 0}, 'categories': ['user_preferences', 'food'], 'created_at': '2025-06-13T07:01:09.645061-07:00', 'updated_at': '2025-06-13T07:01:09.666647-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None}, {'id': '15b4579e-a074-4ef6-a318-03078a18858b', 'memory': 'Prefers sci-fi movies over thriller movies', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'session': 'cloud_demo', 'category': 'cloud_movie_preferences'}, 'categories': ['user_preferences', 'entertainment'], 'created_at': '2025-06-13T07:01:02.776810-07:00', 'updated_at': '2025-06-13T07:01:02.795856-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None}]\n", + "Delete all result: {'message': 'Memories deleted successfully!'}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "๐Ÿ–‡ AgentOps: \u001b[34m\u001b[34mSession Replay for mem0_memoryclient_sync_example.session trace: https://app.agentops.ai/sessions?trace_id=75c9c672ca2f2205d1bdb9b4f615ba2e\u001b[0m\u001b[0m\n", + "๐Ÿ–‡ AgentOps: \u001b[34m\u001b[34mSession Replay for mem0_memoryclient_async_example trace: https://app.agentops.ai/sessions?trace_id=077cb3ff917063d6c4c668dd3de1a5ed\u001b[0m\u001b[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "args: ([{'role': 'user', 'content': \"I'm planning to watch a movie tonight. Any recommendations?\"}, {'role': 'assistant', 'content': 'How about a thriller? They can be quite engaging.'}, {'role': 'user', 'content': \"I'm not a big fan of thriller movies but I love sci-fi movies.\"}, {'role': 'assistant', 'content': \"Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future.\"}],)\n", + "kwargs: {'user_id': 'alice_demo', 'metadata': {'category': 'async_cloud_movies', 'session': 'async_cloud_demo'}}\n", + "return_value: None\n", + "args: None\n", + "kwargs: None\n", + "return_value: \n", + "args: ([{'role': 'user', 'content': 'I prefer dark roast coffee over light roast'}],)\n", + "kwargs: {'user_id': 'alice_demo', 'metadata': {'type': 'async_cloud_preference', 'index': 0}}\n", + "return_value: None\n", + "args: None\n", + "kwargs: None\n", + "return_value: \n", + "args: ([{'role': 'user', 'content': 'I exercise every morning at 6 AM'}],)\n", + "kwargs: {'user_id': 'alice_demo', 'metadata': {'type': 'async_cloud_preference', 'index': 1}}\n", + "return_value: None\n", + "args: None\n", + "kwargs: None\n", + "return_value: \n", + "args: ([{'role': 'user', 'content': \"I'm vegetarian and avoid all meat products\"}],)\n", + "kwargs: {'user_id': 'alice_demo', 'metadata': {'type': 'async_cloud_preference', 'index': 2}}\n", + "return_value: None\n", + "args: None\n", + "kwargs: None\n", + "return_value: \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/events.py:84: DeprecationWarning: output_format='v1.0' is deprecated therefore setting it to 'v1.1' by default.Check out the docs for more information: https://docs.mem0.ai/platform/quickstart#4-1-create-memories\n", + " self._context.run(self._callback, *self._args)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1. {'results': [{'id': '2ef3c2da-4ad1-4b5d-b735-9852d78c17e7', 'event': 'ADD', 'memory': 'Prefers sci-fi movies over thriller movies'}]}\n", + "2. {'results': [{'id': '05b6099e-2269-4ba1-9c8c-476a160e180d', 'event': 'ADD', 'memory': 'Prefers dark roast coffee over light roast'}]}\n", + "3. {'results': [{'id': '38b92272-b8f2-491a-8674-2b37e60fe93a', 'event': 'ADD', 'memory': 'Exercises every morning at 6 AM'}]}\n", + "4. {'results': [{'id': 'e053d1bd-a584-4a82-ba61-eb2b00f299c8', 'event': 'ADD', 'memory': 'Is vegetarian'}, {'id': '2dcee7ea-0684-4ddc-bacd-8a66609649f9', 'event': 'ADD', 'memory': 'Avoids all meat products'}]}\n", + "get_search_attributes args: ('movie preferences',)\n", + "get_search_attributes kwargs: {'user_id': 'alice_demo'}\n", + "get_search_attributes return_value: None\n", + "get_search_attributes args: None\n", + "get_search_attributes kwargs: None\n", + "get_search_attributes return_value: \n", + "get_search_attributes args: ('food preferences',)\n", + "get_search_attributes kwargs: {'user_id': 'alice_demo'}\n", + "get_search_attributes return_value: None\n", + "get_search_attributes args: None\n", + "get_search_attributes kwargs: None\n", + "get_search_attributes return_value: \n", + "get_search_attributes args: ('work information',)\n", + "get_search_attributes kwargs: {'user_id': 'alice_demo'}\n", + "get_search_attributes return_value: None\n", + "get_search_attributes args: None\n", + "get_search_attributes kwargs: None\n", + "get_search_attributes return_value: \n", + "Search 1 result: [{'id': '2ef3c2da-4ad1-4b5d-b735-9852d78c17e7', 'memory': 'Prefers sci-fi movies over thriller movies', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'session': 'async_cloud_demo', 'category': 'async_cloud_movies'}, 'categories': ['user_preferences', 'entertainment'], 'created_at': '2025-06-13T07:01:33.908188-07:00', 'updated_at': '2025-06-13T07:01:33.928443-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.5863419103332085}, {'id': '05b6099e-2269-4ba1-9c8c-476a160e180d', 'memory': 'Prefers dark roast coffee over light roast', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'async_cloud_preference', 'index': 0}, 'categories': ['user_preferences', 'food'], 'created_at': '2025-06-13T07:01:33.765858-07:00', 'updated_at': '2025-06-13T07:01:33.785855-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.4206878417525355}, {'id': '2dcee7ea-0684-4ddc-bacd-8a66609649f9', 'memory': 'Avoids all meat products', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'async_cloud_preference', 'index': 2}, 'categories': None, 'created_at': '2025-06-13T07:01:36.120569-07:00', 'updated_at': '2025-06-13T07:01:36.138808-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.3659444262368807}, {'id': '38b92272-b8f2-491a-8674-2b37e60fe93a', 'memory': 'Exercises every morning at 6 AM', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'async_cloud_preference', 'index': 1}, 'categories': ['health'], 'created_at': '2025-06-13T07:01:33.281702-07:00', 'updated_at': '2025-06-13T07:01:33.300498-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.3010108740041443}]\n", + "Search 2 result: [{'id': '05b6099e-2269-4ba1-9c8c-476a160e180d', 'memory': 'Prefers dark roast coffee over light roast', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'async_cloud_preference', 'index': 0}, 'categories': ['user_preferences', 'food'], 'created_at': '2025-06-13T07:01:33.765858-07:00', 'updated_at': '2025-06-13T07:01:33.785855-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.5045933422688647}, {'id': '2dcee7ea-0684-4ddc-bacd-8a66609649f9', 'memory': 'Avoids all meat products', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'async_cloud_preference', 'index': 2}, 'categories': None, 'created_at': '2025-06-13T07:01:36.120569-07:00', 'updated_at': '2025-06-13T07:01:36.138808-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.48612332344055176}, {'id': '2ef3c2da-4ad1-4b5d-b735-9852d78c17e7', 'memory': 'Prefers sci-fi movies over thriller movies', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'session': 'async_cloud_demo', 'category': 'async_cloud_movies'}, 'categories': ['user_preferences', 'entertainment'], 'created_at': '2025-06-13T07:01:33.908188-07:00', 'updated_at': '2025-06-13T07:01:33.928443-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.43332618077553475}, {'id': 'e053d1bd-a584-4a82-ba61-eb2b00f299c8', 'memory': 'Is vegetarian', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'async_cloud_preference', 'index': 2}, 'categories': ['user_preferences', 'food'], 'created_at': '2025-06-13T07:01:34.655396-07:00', 'updated_at': '2025-06-13T07:01:34.728349-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.3886008858680725}, {'id': '38b92272-b8f2-491a-8674-2b37e60fe93a', 'memory': 'Exercises every morning at 6 AM', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'async_cloud_preference', 'index': 1}, 'categories': ['health'], 'created_at': '2025-06-13T07:01:33.281702-07:00', 'updated_at': '2025-06-13T07:01:33.300498-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.3528817804385299}]\n", + "Search 3 result: [{'id': '2dcee7ea-0684-4ddc-bacd-8a66609649f9', 'memory': 'Avoids all meat products', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'async_cloud_preference', 'index': 2}, 'categories': None, 'created_at': '2025-06-13T07:01:36.120569-07:00', 'updated_at': '2025-06-13T07:01:36.138808-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.3600524282486701}, {'id': '38b92272-b8f2-491a-8674-2b37e60fe93a', 'memory': 'Exercises every morning at 6 AM', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'async_cloud_preference', 'index': 1}, 'categories': ['health'], 'created_at': '2025-06-13T07:01:33.281702-07:00', 'updated_at': '2025-06-13T07:01:33.300498-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.3572185167334696}, {'id': '05b6099e-2269-4ba1-9c8c-476a160e180d', 'memory': 'Prefers dark roast coffee over light roast', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'async_cloud_preference', 'index': 0}, 'categories': ['user_preferences', 'food'], 'created_at': '2025-06-13T07:01:33.765858-07:00', 'updated_at': '2025-06-13T07:01:33.785855-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.32800289988517994}, {'id': 'e053d1bd-a584-4a82-ba61-eb2b00f299c8', 'memory': 'Is vegetarian', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'async_cloud_preference', 'index': 2}, 'categories': ['user_preferences', 'food'], 'created_at': '2025-06-13T07:01:34.655396-07:00', 'updated_at': '2025-06-13T07:01:34.728349-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.31821893562191406}, {'id': '2ef3c2da-4ad1-4b5d-b735-9852d78c17e7', 'memory': 'Prefers sci-fi movies over thriller movies', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'session': 'async_cloud_demo', 'category': 'async_cloud_movies'}, 'categories': ['user_preferences', 'entertainment'], 'created_at': '2025-06-13T07:01:33.908188-07:00', 'updated_at': '2025-06-13T07:01:33.928443-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None, 'score': 0.3099285733614652}]\n", + "Async cloud memories: [{'id': '2dcee7ea-0684-4ddc-bacd-8a66609649f9', 'memory': 'Avoids all meat products', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'async_cloud_preference', 'index': 2}, 'categories': ['food'], 'created_at': '2025-06-13T07:01:36.120569-07:00', 'updated_at': '2025-06-13T07:01:36.138808-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None}, {'id': 'e053d1bd-a584-4a82-ba61-eb2b00f299c8', 'memory': 'Is vegetarian', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'async_cloud_preference', 'index': 2}, 'categories': ['user_preferences', 'food'], 'created_at': '2025-06-13T07:01:34.655396-07:00', 'updated_at': '2025-06-13T07:01:34.728349-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None}, {'id': '2ef3c2da-4ad1-4b5d-b735-9852d78c17e7', 'memory': 'Prefers sci-fi movies over thriller movies', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'session': 'async_cloud_demo', 'category': 'async_cloud_movies'}, 'categories': ['user_preferences', 'entertainment'], 'created_at': '2025-06-13T07:01:33.908188-07:00', 'updated_at': '2025-06-13T07:01:33.928443-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None}, {'id': '05b6099e-2269-4ba1-9c8c-476a160e180d', 'memory': 'Prefers dark roast coffee over light roast', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'async_cloud_preference', 'index': 0}, 'categories': ['user_preferences', 'food'], 'created_at': '2025-06-13T07:01:33.765858-07:00', 'updated_at': '2025-06-13T07:01:33.785855-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None}, {'id': '38b92272-b8f2-491a-8674-2b37e60fe93a', 'memory': 'Exercises every morning at 6 AM', 'user_id': 'alice_demo', 'actor_id': None, 'metadata': {'type': 'async_cloud_preference', 'index': 1}, 'categories': ['health'], 'created_at': '2025-06-13T07:01:33.281702-07:00', 'updated_at': '2025-06-13T07:01:33.300498-07:00', 'expiration_date': None, 'structured_attributes': None, 'internal_metadata': None, 'deleted_at': None}]\n", + "Delete all result: {'message': 'Memories deleted successfully!'}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "๐Ÿ–‡ AgentOps: \u001b[34m\u001b[34mSession Replay for mem0_memoryclient_async_example.session trace: https://app.agentops.ai/sessions?trace_id=077cb3ff917063d6c4c668dd3de1a5ed\u001b[0m\u001b[0m\n" + ] + } + ], + "source": [ + "# Execute both sync and async demonstrations\n", + "demonstrate_sync_memory_client(sample_messages, sample_preferences, user_id)\n", + "await demonstrate_async_memory_client(sample_messages, sample_preferences, user_id)" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + }, + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/mem0/mem0_memoryclient_example.py b/examples/mem0/mem0_memoryclient_example.py new file mode 100644 index 000000000..c99c52986 --- /dev/null +++ b/examples/mem0/mem0_memoryclient_example.py @@ -0,0 +1,186 @@ +""" +# Cloud Memory Operations with Mem0 MemoryClient + +This example demonstrates how to use Mem0's cloud-based MemoryClient for managing conversational memory and user preferences with both synchronous and asynchronous operations. + +## Overview + +This example showcases cloud-based memory management operations where we: + +1. **Initialize MemoryClient instances** for both sync and async cloud operations +2. **Store conversation history** in the cloud with rich metadata +3. **Perform concurrent operations** using async/await patterns +4. **Search and filter memories** using natural language and structured queries + +By using the cloud-based MemoryClient with async operations, you can leverage Mem0's managed infrastructure while performing multiple memory operations simultaneously. This is ideal for production applications that need scalable memory management without managing local storage. +""" +import os +import asyncio +from dotenv import load_dotenv + +# Load environment variables first +load_dotenv() + +# Set environment variables before importing +os.environ["AGENTOPS_API_KEY"] = os.getenv("AGENTOPS_API_KEY") +os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY") +mem0_api_key = os.getenv("MEM0_API_KEY") + +# Import agentops BEFORE mem0 to ensure proper instrumentation +import agentops + +# Now import mem0 - it will be instrumented by agentops +from mem0 import MemoryClient, AsyncMemoryClient + + +def demonstrate_sync_memory_client(sample_messages, sample_preferences, user_id): + """ + Demonstrate synchronous MemoryClient operations with cloud storage. + + This function performs sequential cloud memory operations including: + - Initializing cloud-based memory client with API authentication + - Adding conversation messages to cloud storage + - Storing user preferences with metadata + - Searching memories using natural language + - Retrieving memories with filters + - Cleaning up cloud memories + + Args: + sample_messages: List of conversation messages to store + sample_preferences: List of user preferences to store + user_id: Unique identifier for the user + + Cloud benefit: All memory operations are handled by Mem0's infrastructure, + providing scalability and persistence without local storage management. + """ + agentops.start_trace("mem0_memoryclient_example",tags=["mem0_memoryclient_example"]) + try: + # Initialize sync MemoryClient with API key for cloud access + client = MemoryClient(api_key=mem0_api_key) + + + # Add conversation to cloud storage with metadata + result = client.add( + sample_messages, user_id=user_id, metadata={"category": "cloud_movie_preferences", "session": "cloud_demo"} + ) + print(f"Add result: {result}") + + # Add preferences sequentially to cloud + for i, preference in enumerate(sample_preferences[:3]): # Limit for demo + result = client.add(preference, user_id=user_id, metadata={"type": "cloud_preference", "index": i}) + + + # 2. SEARCH operations - leverage cloud search capabilities + search_result = client.search("What are the user's movie preferences?", user_id=user_id) + print(f"Search result: {search_result}") + + # 3. GET_ALL with filters - demonstrate structured query capabilities + filters = {"AND": [{"user_id": user_id}]} + all_memories = client.get_all(filters=filters, limit=10) + print(f"Cloud memories retrieved: {all_memories}") + + # Cleanup - remove all user memories from cloud + delete_all_result = client.delete_all(user_id=user_id) + print(f"Delete all result: {delete_all_result}") + agentops.end_trace(end_state="success") + except Exception as e: + agentops.end_trace(end_state="error") + + +async def demonstrate_async_memory_client(sample_messages, sample_preferences, user_id): + """ + Demonstrate asynchronous MemoryClient operations with concurrent cloud access. + + This function performs concurrent cloud memory operations including: + - Initializing async cloud-based memory client + - Adding multiple memories concurrently using asyncio.gather() + - Performing parallel search operations across cloud storage + - Retrieving filtered memories asynchronously + - Cleaning up cloud memories efficiently + + Args: + sample_messages: List of conversation messages to store + sample_preferences: List of user preferences to store + user_id: Unique identifier for the user + + Performance benefit: Async operations allow multiple cloud API calls to execute + concurrently, significantly reducing total execution time compared to sequential calls. + This is especially beneficial when dealing with network I/O to cloud services. + """ + agentops.start_trace("mem0_memoryclient_example",tags=["mem0_memoryclient_example"]) + try: + # Initialize async MemoryClient for concurrent cloud operations + async_client = AsyncMemoryClient(api_key=mem0_api_key) + + # Add conversation and preferences concurrently to cloud + add_conversation_task = async_client.add( + sample_messages, user_id=user_id, metadata={"category": "async_cloud_movies", "session": "async_cloud_demo"} + ) + + # Create tasks for adding preferences in parallel + add_preference_tasks = [ + async_client.add(pref, user_id=user_id, metadata={"type": "async_cloud_preference", "index": i}) + for i, pref in enumerate(sample_preferences[:3]) + ] + + # Execute all add operations concurrently + results = await asyncio.gather(add_conversation_task, *add_preference_tasks) + for i, result in enumerate(results): + print(f"{i+1}. {result}") + + # 2. Concurrent SEARCH operations - multiple cloud searches in parallel + search_tasks = [ + async_client.search("movie preferences", user_id=user_id), + async_client.search("food preferences", user_id=user_id), + async_client.search("work information", user_id=user_id), + ] + + # Execute all searches concurrently + search_results = await asyncio.gather(*search_tasks) + for i, result in enumerate(search_results): + print(f"Search {i+1} result: {result}") + + # 3. GET_ALL operation - retrieve filtered memories from cloud + filters = {"AND": [{"user_id": user_id}]} + all_memories = await async_client.get_all(filters=filters, limit=10) + print(f"Async cloud memories: {all_memories}") + + # Final cleanup - remove all memories asynchronously + delete_all_result = await async_client.delete_all(user_id=user_id) + print(f"Delete all result: {delete_all_result}") + + agentops.end_trace(end_state="success") + + except Exception as e: + agentops.end_trace(end_state="error") + + +# Sample user data for demonstration +user_id = "alice_demo" +agent_id = "assistant_demo" +run_id = "session_001" + +# Sample conversation data demonstrating preference discovery through dialogue +sample_messages = [ + {"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"}, + {"role": "assistant", "content": "How about a thriller? They can be quite engaging."}, + {"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."}, + { + "role": "assistant", + "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future.", + }, +] + +# Sample user preferences representing various personal attributes +sample_preferences = [ + "I prefer dark roast coffee over light roast", + "I exercise every morning at 6 AM", + "I'm vegetarian and avoid all meat products", + "I love reading science fiction novels", + "I work in software engineering", +] + +# Execute both sync and async demonstrations +# Note: The async version typically completes faster due to concurrent operations +demonstrate_sync_memory_client(sample_messages, sample_preferences, user_id) +asyncio.run(demonstrate_async_memory_client(sample_messages, sample_preferences, user_id)) \ No newline at end of file diff --git a/examples/mem0_examples/README.md b/examples/mem0_examples/README.md deleted file mode 100644 index 8c33e0c86..000000000 --- a/examples/mem0_examples/README.md +++ /dev/null @@ -1,227 +0,0 @@ -# Comprehensive Mem0 Example with AgentOps Instrumentation - -This example demonstrates all four mem0 memory classes with full AgentOps instrumentation: - -## Memory Classes Covered - -1. **Memory** (Sync Local Memory) - Direct memory operations using local vector stores -2. **AsyncMemory** (Async Local Memory) - Asynchronous memory operations with local storage -3. **MemoryClient** (Sync Cloud Client) - Synchronous operations using mem0's cloud service -4. **AsyncMemoryClient** (Async Cloud Client) - Asynchronous operations with mem0 cloud - -## Features Demonstrated - -### All Classes Support: -- **ADD** - Store conversations and individual memories -- **SEARCH** - Semantic search through memories -- **GET_ALL** - Retrieve all memories with optional filters -- **GET** - Fetch specific memory by ID -- **UPDATE** - Modify existing memories -- **DELETE** - Remove specific memories -- **DELETE_ALL** - Clear all memories for a user -- **HISTORY** - View memory change history (local classes only) - -### AgentOps Instrumentation: -- Complete tracing of all memory operations -- Detailed span attributes for debugging -- Async operation support -- Metadata and user tracking -- Performance metrics - -## Quick Start - -### 1. Install Dependencies - -```bash -pip install -r requirements_mem0_example.txt -``` - -### 2. Set Environment Variables - -Create a `.env` file with the following variables: - -```env -# Required -AGENTOPS_API_KEY=your_agentops_api_key -OPENAI_API_KEY=your_openai_api_key - -# Optional (for cloud operations) -MEM0_API_KEY=your_mem0_cloud_api_key - -# Optional (for alternative LLM providers) -ANTHROPIC_API_KEY=your_anthropic_api_key -``` - -### 4. Run the Example - -```bash -python comprehensive_mem0_example.py -``` - -## ๐Ÿ“‹ What You'll See - -The example will run through four comprehensive demonstrations: - -### Sync Memory (Local) -``` - SYNC MEMORY (Local) OPERATIONS -=========================================================== - Sync Memory initialized successfully - - Adding memories... - Added conversation: {'message': 'Memory added successfully.'} - Added preference 1: {'message': 'Memory added successfully.'} - ... - - Searching memories... - Query: 'What movies does the user like?' - Result 1: User strongly prefers sci-fi movies and dislikes thrillers - ... -``` - -### Async Memory (Local) -Shows concurrent operations and async/await patterns: -``` - ASYNC MEMORY (Local) OPERATIONS -=========================================================== - Async Memory initialized successfully - - Adding memories asynchronously... - Added conversation: {'message': 'Memory added successfully.'} - ... - - Performing concurrent operations... - Get result: {'id': 'mem_123...', 'memory': '...'} - Update result: {'message': 'Memory updated successfully.'} - History entries: 3 -``` - -### Sync Memory Client (Cloud) -Demonstrates cloud-based memory operations: -``` - SYNC MEMORY CLIENT (Cloud) OPERATIONS -=========================================================== -Sync MemoryClient initialized successfully - - Adding memories to cloud... - Added conversation to cloud: {'id': 'cloud_mem_456...'} - ... -``` - -### Async Memory Client (Cloud) -Shows async cloud operations with concurrent processing: -``` -ASYNC MEMORY CLIENT (Cloud) OPERATIONS -=========================================================== -Async MemoryClient initialized successfully - -Adding memories to cloud asynchronously... - Added conversation and preferences: 4 items - ... - -Performing concurrent searches... - Search 1 result: {'results': [...]} - ... -``` - -## Configuration Options - -### Local Memory Configuration - -```python -local_config = { - "llm": { - "provider": "openai", # or "anthropic", "ollama", etc. - "config": { - "model": "gpt-4o-mini", - "temperature": 0.1, - "max_tokens": 2000, - "api_key": "your_api_key", - } - }, - "embedder": { - "provider": "openai", # or "huggingface", "sentence-transformers" - "config": { - "model": "text-embedding-3-small", - "api_key": "your_api_key", - } - }, - "vector_store": { - "provider": "qdrant", # or "chromadb", "pinecone" - "config": { - "collection_name": "test_collection", - "host": "localhost", - "port": 6333, - } - } -} -``` - -### Cloud Client Configuration - -```python -# Simply provide your API key -client = MemoryClient(api_key="your_mem0_api_key") -async_client = AsyncMemoryClient(api_key="your_mem0_api_key") -``` - -## AgentOps Dashboard - -After running the example, check your AgentOps dashboard to see: - -- **Spans**: Detailed trace of each memory operation -- **Metrics**: Performance data and operation counts -- **Attributes**: Memory content, user IDs, metadata -- **Errors**: Any failures with full stack traces - -## Customization - -### Add Your Own Operations - -```python -# Extend the Mem0Example class -class CustomMem0Example(Mem0Example): - def custom_memory_workflow(self): - memory = Memory.from_config(self.local_config) - - # Your custom logic here - result = memory.add("Custom memory content", user_id="custom_user") - search_results = memory.search("custom query", user_id="custom_user") - - return search_results -``` - -### Use Different Providers - -```python -# Anthropic LLM -anthropic_config = { - "llm": { - "provider": "anthropic", - "config": { - "model": "claude-3-haiku-20240307", - "api_key": os.getenv("ANTHROPIC_API_KEY"), - } - } -} - -# Hugging Face Embeddings -hf_config = { - "embedder": { - "provider": "huggingface", - "config": { - "model": "sentence-transformers/all-MiniLM-L6-v2", - } - } -} -``` - -## Learn More -- [Mem0 Documentation](https://docs.mem0.ai/) -- [AgentOps Documentation](https://docs.agentops.ai/) -- [OpenAI API Reference](https://platform.openai.com/docs/) -- [Qdrant Documentation](https://qdrant.tech/documentation/) - -## ๐Ÿ“„ License - -This example is provided under the same license as the AgentOps project. \ No newline at end of file diff --git a/examples/mem0_examples/comprehensive_mem0_example.py b/examples/mem0_examples/comprehensive_mem0_example.py deleted file mode 100644 index 5dffd4616..000000000 --- a/examples/mem0_examples/comprehensive_mem0_example.py +++ /dev/null @@ -1,373 +0,0 @@ -""" -Comprehensive Mem0 Example with AgentOps Instrumentation - -This example demonstrates all four mem0 memory classes: -1. Memory (sync local memory) -2. AsyncMemory (async local memory) -3. MemoryClient (sync cloud client) -4. AsyncMemoryClient (async cloud client) - -Each section shows CRUD operations and demonstrates AgentOps instrumentation. -""" - -import os -import asyncio -import logging -from dotenv import load_dotenv - -# Load environment variables -load_dotenv() - -# CRITICAL: Initialize AgentOps BEFORE importing mem0 classes -# This ensures proper instrumentation and context propagation -import agentops # noqa: E402 -from mem0 import Memory, AsyncMemory, MemoryClient, AsyncMemoryClient # noqa: E402 - - -# Initialize AgentOps -agentops.init(os.getenv("AGENTOPS_API_KEY")) - - -# Set up logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# Configuration for local memory (Memory and AsyncMemory) -local_config = { - "llm": { - "provider": "openai", - "config": { - "model": "gpt-4o-mini", - "temperature": 0.1, - "max_tokens": 2000, - "api_key": os.getenv("OPENAI_API_KEY"), - }, - } -} - -# API key for cloud clients (MemoryClient and AsyncMemoryClient) -mem0_api_key = os.getenv("MEM0_API_KEY") - -# Sample user data -user_id = "alice_demo" -agent_id = "assistant_demo" -run_id = "session_001" - -# Sample conversation data -sample_messages = [ - {"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"}, - {"role": "assistant", "content": "How about a thriller? They can be quite engaging."}, - {"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."}, - { - "role": "assistant", - "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future.", - }, -] - -sample_preferences = [ - "I prefer dark roast coffee over light roast", - "I exercise every morning at 6 AM", - "I'm vegetarian and avoid all meat products", - "I love reading science fiction novels", - "I work in software engineering", -] - - -def demonstrate_sync_memory(): - """Demonstrate sync Memory class operations.""" - print("\n" + "=" * 60) - print("๐Ÿง  SYNC MEMORY (Local) OPERATIONS") - print("=" * 60) - - try: - # Initialize sync Memory - memory = Memory.from_config(local_config) - print("โœ… Sync Memory initialized successfully") - - # 1. ADD operations - print("\n๐Ÿ“ Adding memories...") - - # Add conversation messages - result = memory.add( - sample_messages, user_id=user_id, metadata={"category": "movie_preferences", "session": "demo"} - ) - print(f" ๐Ÿ“Œ Added conversation: {result}") - - # Add individual preferences - for i, preference in enumerate(sample_preferences): - result = memory.add(preference, user_id=user_id, metadata={"type": "preference", "index": i}) - print(f" ๐Ÿ“Œ Added preference {i+1}: {result}") - - # 2. SEARCH operations - print("\n๐Ÿ” Searching memories...") - search_queries = [ - "What movies does the user like?", - "What are the user's food preferences?", - "When does the user exercise?", - ] - - for query in search_queries: - results = memory.search(query, user_id=user_id) - print(f" ๐Ÿ”Ž Query: '{query}'") - if results and "results" in results: - for j, result in enumerate(results["results"][:2]): # Show top 2 - print(f" ๐Ÿ’ก Result {j+1}: {result.get('memory', 'N/A')}") - else: - print(" โŒ No results found") - - # 3. GET_ALL operations - print("\n๐Ÿ“‹ Getting all memories...") - all_memories = memory.get_all(user_id=user_id) - if all_memories and "results" in all_memories: - print(f" ๐Ÿ“Š Total memories: {len(all_memories['results'])}") - for i, mem in enumerate(all_memories["results"][:3]): # Show first 3 - print(f" {i+1}. ID: {mem.get('id', 'N/A')[:8]}... | {mem.get('memory', 'N/A')[:50]}...") - - # Cleanup - print("\n๐Ÿงน Cleaning up all memories...") - delete_all_result = memory.delete_all(user_id=user_id) - print(f" โœ… Delete all result: {delete_all_result}") - - except Exception as e: - print(f"โŒ Sync Memory error: {e}") - logger.error(f"Sync Memory demonstration failed: {e}") - - -async def demonstrate_async_memory(): - """Demonstrate async Memory class operations.""" - print("\n" + "=" * 60) - print("๐Ÿš€ ASYNC MEMORY (Local) OPERATIONS") - print("=" * 60) - - try: - # Initialize async Memory - async_memory = AsyncMemory.from_config(local_config) - print("โœ… Async Memory initialized successfully") - - # 1. ADD operations - print("\n๐Ÿ“ Adding memories asynchronously...") - - # Add conversation messages - result = await async_memory.add( - sample_messages, user_id=user_id, metadata={"category": "async_movie_preferences", "session": "async_demo"} - ) - print(f" ๐Ÿ“Œ Added conversation: {result}") - - # Add preferences concurrently - async def add_preference(preference, index): - return await async_memory.add( - preference, user_id=user_id, metadata={"type": "async_preference", "index": index} - ) - - tasks = [add_preference(pref, i) for i, pref in enumerate(sample_preferences)] - results = await asyncio.gather(*tasks) - for i, result in enumerate(results): - print(f" ๐Ÿ“Œ Added async preference {i+1}: {result}") - - # 2. SEARCH operations - print("\n๐Ÿ” Searching memories asynchronously...") - search_queries = [ - "What movies does the user like?", - "What are the user's dietary restrictions?", - "What does the user do for work?", - ] - - async def search_memory(query): - return await async_memory.search(query, user_id=user_id), query - - search_tasks = [search_memory(query) for query in search_queries] - search_results = await asyncio.gather(*search_tasks) - - for result, query in search_results: - print(f" ๐Ÿ”Ž Query: '{query}'") - if result and "results" in result: - for j, res in enumerate(result["results"][:2]): - print(f" ๐Ÿ’ก Result {j+1}: {res.get('memory', 'N/A')}") - else: - print(" โŒ No results found") - - # 3. GET_ALL operations - print("\n๐Ÿ“‹ Getting all memories asynchronously...") - all_memories = await async_memory.get_all(user_id=user_id) - if all_memories and "results" in all_memories: - print(f" ๐Ÿ“Š Total async memories: {len(all_memories['results'])}") - for i, mem in enumerate(all_memories["results"][:3]): - print(f" {i+1}. ID: {mem.get('id', 'N/A')[:8]}... | {mem.get('memory', 'N/A')[:50]}...") - - # Cleanup - print("\n๐Ÿงน Cleaning up all async memories...") - delete_all_result = await async_memory.delete_all(user_id=user_id) - print(f" โœ… Delete all result: {delete_all_result}") - - except Exception as e: - print(f"โŒ Async Memory error: {e}") - logger.error(f"Async Memory demonstration failed: {e}") - - -def demonstrate_sync_memory_client(): - """Demonstrate sync MemoryClient class operations.""" - print("\n" + "=" * 60) - print("โ˜๏ธ SYNC MEMORY CLIENT (Cloud) OPERATIONS") - print("=" * 60) - - if not mem0_api_key: - print("โŒ MEM0_API_KEY not found. Skipping cloud client operations.") - return - - try: - # Initialize sync MemoryClient - client = MemoryClient(api_key=mem0_api_key) - print("โœ… Sync MemoryClient initialized successfully") - - # 1. ADD operations - print("\n๐Ÿ“ Adding memories to cloud...") - - # Add conversation - result = client.add( - sample_messages, user_id=user_id, metadata={"category": "cloud_movie_preferences", "session": "cloud_demo"} - ) - print(f" ๐Ÿ“Œ Added conversation to cloud: {result}") - - # Add preferences - for i, preference in enumerate(sample_preferences[:3]): # Limit for demo - result = client.add(preference, user_id=user_id, metadata={"type": "cloud_preference", "index": i}) - print(f" ๐Ÿ“Œ Added cloud preference {i+1}: {result}") - - # 2. SEARCH operations - print("\n๐Ÿ” Searching cloud memories...") - search_result = client.search("What are the user's movie preferences?", user_id=user_id) - print(f" ๐Ÿ”Ž Search result: {search_result}") - - # 3. GET_ALL with filters - print("\n๐Ÿ“‹ Getting all cloud memories with filters...") - filters = {"AND": [{"user_id": user_id}]} - all_memories = client.get_all(filters=filters, limit=10) - print(f" ๐Ÿ“Š Cloud memories retrieved: {all_memories}") - - # Cleanup - print("\n๐Ÿงน Cleaning up cloud memories...") - delete_all_result = client.delete_all(user_id=user_id) - print(f" โœ… Delete all result: {delete_all_result}") - - except Exception as e: - print(f"โŒ Sync MemoryClient error: {e}") - logger.error(f"Sync MemoryClient demonstration failed: {e}") - - -async def demonstrate_async_memory_client(): - """Demonstrate async MemoryClient class operations.""" - print("\n" + "=" * 60) - print("๐ŸŒ ASYNC MEMORY CLIENT (Cloud) OPERATIONS") - print("=" * 60) - - if not mem0_api_key: - print("โŒ MEM0_API_KEY not found. Skipping async cloud client operations.") - return - - try: - # Initialize async MemoryClient - async_client = AsyncMemoryClient(api_key=mem0_api_key) - print("โœ… Async MemoryClient initialized successfully") - - # 1. ADD operations concurrently - print("\n๐Ÿ“ Adding memories to cloud asynchronously...") - - # Add conversation and preferences concurrently - add_conversation_task = async_client.add( - sample_messages, user_id=user_id, metadata={"category": "async_cloud_movies", "session": "async_cloud_demo"} - ) - - add_preference_tasks = [ - async_client.add(pref, user_id=user_id, metadata={"type": "async_cloud_preference", "index": i}) - for i, pref in enumerate(sample_preferences[:3]) - ] - - results = await asyncio.gather(add_conversation_task, *add_preference_tasks) - print(f" ๐Ÿ“Œ Added conversation and preferences: {len(results)} items") - for i, result in enumerate(results): - print(f" {i+1}. {result}") - - # 2. Concurrent SEARCH operations - print("\n๐Ÿ” Performing concurrent searches...") - search_tasks = [ - async_client.search("movie preferences", user_id=user_id), - async_client.search("food preferences", user_id=user_id), - async_client.search("work information", user_id=user_id), - ] - - search_results = await asyncio.gather(*search_tasks) - for i, result in enumerate(search_results): - print(f" ๐Ÿ”Ž Search {i+1} result: {result}") - - # 3. GET_ALL operation - print("\n๐Ÿ“‹ Getting all async cloud memories...") - filters = {"AND": [{"user_id": user_id}]} - all_memories = await async_client.get_all(filters=filters, limit=10) - print(f" ๐Ÿ“Š Async cloud memories: {all_memories}") - - # Final cleanup - print("\n๐Ÿงน Final cleanup of async cloud memories...") - delete_all_result = await async_client.delete_all(user_id=user_id) - print(f" โœ… Delete all result: {delete_all_result}") - - except Exception as e: - print(f"โŒ Async MemoryClient error: {e}") - logger.error(f"Async MemoryClient demonstration failed: {e}") - - -def check_environment(): - """Check if required environment variables are set.""" - required_vars = ["AGENTOPS_API_KEY", "OPENAI_API_KEY"] - optional_vars = ["MEM0_API_KEY"] - - missing_required = [var for var in required_vars if not os.getenv(var)] - missing_optional = [var for var in optional_vars if not os.getenv(var)] - - if missing_required: - print(f"โŒ Missing required environment variables: {missing_required}") - return False - - if missing_optional: - print(f"โš ๏ธ Missing optional environment variables: {missing_optional}") - print(" Cloud client operations will be skipped.") - - return True - - -async def main(): - """Run the complete demonstration of all mem0 classes.""" - print("๐ŸŽฌ STARTING COMPREHENSIVE MEM0 DEMONSTRATION") - print("This demo showcases all four mem0 classes with AgentOps instrumentation:") - print("1. Memory (sync local)") - print("2. AsyncMemory (async local)") - print("3. MemoryClient (sync cloud)") - print("4. AsyncMemoryClient (async cloud)") - print("\n" + "=" * 80) - - if not check_environment(): - print("Please set the required environment variables and try again.") - return - - try: - # Run all demonstrations - demonstrate_sync_memory() - await demonstrate_async_memory() - demonstrate_sync_memory_client() - await demonstrate_async_memory_client() - - print("\n" + "=" * 80) - print("โœ… COMPREHENSIVE MEM0 DEMONSTRATION COMPLETED!") - print("Check your AgentOps dashboard to see the instrumentation data.") - - except Exception as e: - logger.error(f"Demo failed: {e}") - print(f"โŒ Demo failed: {e}") - - finally: - # End AgentOps session - agentops.end_session("Success") - - -if __name__ == "__main__": - # Run the async main function - asyncio.run(main()) From c6f46e1befbb5bdbde053e04c4f254ee884e3b82 Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Fri, 13 Jun 2025 20:50:36 +0530 Subject: [PATCH 17/23] ruff checks --- examples/mem0/mem0_memory_basic_example.py | 142 +++++++++++++++++++++ examples/mem0/mem0_memory_example.py | 42 +++--- 2 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 examples/mem0/mem0_memory_basic_example.py diff --git a/examples/mem0/mem0_memory_basic_example.py b/examples/mem0/mem0_memory_basic_example.py new file mode 100644 index 000000000..5318bfbad --- /dev/null +++ b/examples/mem0/mem0_memory_basic_example.py @@ -0,0 +1,142 @@ +""" +Basic Memory Operations with Mem0: Local vs Cloud + +This example demonstrates the two primary ways to use Mem0 for memory management: +1. Memory class - For local, self-hosted memory storage +2. MemoryClient class - For cloud-based memory storage using Mem0's managed service + +Both approaches offer the same core functionality (add, search, get_all) but differ in: +- Storage location: Local vs Mem0's cloud infrastructure +- Setup requirements: Local configuration vs API key authentication +- Use cases: Development/self-hosted vs production/scalable applications + +Key features demonstrated: +- Adding individual memories with metadata +- Storing conversation history +- Searching memories using natural language +- Retrieving all memories for a user + +This example runs both approaches sequentially to showcase the similarities and differences. +""" + +import os +import asyncio +from dotenv import load_dotenv +from mem0 import Memory,MemoryClient +import agentops + +# Load environment variables from .env file +load_dotenv() + +# Set up API keys for AgentOps tracing +os.environ["AGENTOPS_API_KEY"] = os.getenv("AGENTOPS_API_KEY") +os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY") +api_key= os.getenv("MEM0_API_KEY") + +# Configuration for Memory with OpenAI as the LLM provider +# This configuration specifies which model to use and its parameters +openai_config = { + "llm": { + "provider": "openai", + "config": { + "model": "gpt-4o-mini", + "temperature": 0.1, # Low temperature for consistent outputs + "max_tokens": 2000, + "api_key": os.getenv("OPENAI_API_KEY"), + } + } +} + +# Sample conversation data demonstrating user preferences +# This shows how Mem0 can extract and remember information from natural dialogue +test_messages = [ + {"role": "user", "content": "I love science fiction movies and books."}, + {"role": "assistant", "content": "That's great! Sci-fi offers amazing worlds and concepts."}, + {"role": "user", "content": "I also prefer dark roast coffee over light roast."}, +] + +# Start AgentOps trace for monitoring and debugging +agentops.start_trace("mem0_memory_basic_example", tags=["mem0_memory_basic_example"]) +try: + # Initialize Memory instance with the OpenAI configuration + # This creates a local memory store that processes and stores memories on your infrastructure + m_sync = Memory.from_config(openai_config) + + # This demonstrates storing individual facts with metadata + result = m_sync.add( + [{"role": "user", "content": "I like to drink coffee in the morning and go for a walk."}], + user_id="alice_sync", + metadata={"category": "preferences", "type": "sync"} + ) + + # This shows how to store entire conversations that Mem0 will analyze for memorable information + result = m_sync.add( + test_messages, + user_id="alice_sync", + metadata={"category": "conversation", "type": "sync"} + ) + + # Demonstrates how to query stored memories with questions + search_result = m_sync.search("What does the user like to drink?",user_id="alice_sync") + + # Shows how to get a complete view of what Mem0 remembers about a user + all_memories = m_sync.get_all(user_id="alice_sync") + + + # Display all memories for inspection + if all_memories.get('results'): + for i, memory in enumerate(all_memories['results'], 1): + print(f"{i}. {memory.get('memory', 'N/A')}") + + # Successfully completed all operations + agentops.end_trace(end_state="success") + +except Exception as e: + # Log any errors that occur during execution + agentops.end_trace(end_state="error") + + +# Start AgentOps trace for monitoring and debugging +agentops.start_trace("mem0_memoryclient_basic_example", tags=["mem0_memoryclient_basic_example"]) +try: + # Initialize MemoryClient with API key for cloud access + # This connects to Mem0's cloud infrastructure for scalable memory management + m_sync = MemoryClient(api_key=api_key) + + # This demonstrates storing individual facts with metadata + result = m_sync.add( + [{"role": "user", "content": "I like to drink coffee in the morning and go for a walk."}], + user_id="alice_sync", + metadata={"category": "preferences", "type": "sync"} + ) + + # This shows how to store entire conversations that Mem0 will analyze for memorable information + result = m_sync.add( + test_messages, + user_id="alice_sync", + ) + filters = { + "AND": [ + { + "user_id": user_id + }, + ] + } + # Demonstrates how to query stored memories with questions + search_result = m_sync.search(version="v2",query="What does the user like to drink?",filters=filters) + print(f"Cloud search results: {search_result}") + + # Shows how to get all memories stored in Mem0's cloud for a user + all_memories = m_sync.get_all(version="v2",filters=filters,page_size=1) + print(f"Total cloud memories: {len(all_memories.get('results', []))}") + + # Display all memories for inspection + if all_memories.get('results'): + for i, memory in enumerate(all_memories['results'], 1): + print(f"{i}. {memory.get('memory', 'N/A')}") + + # Successfully completed all operations + agentops.end_trace(end_state="success") +except Exception as e: + # Log any errors that occur during execution + agentops.end_trace(end_state="error") diff --git a/examples/mem0/mem0_memory_example.py b/examples/mem0/mem0_memory_example.py index b891111a4..58b80b8c6 100644 --- a/examples/mem0/mem0_memory_example.py +++ b/examples/mem0/mem0_memory_example.py @@ -16,7 +16,6 @@ """ import os import asyncio -import logging from dotenv import load_dotenv # Load environment variables first @@ -30,48 +29,47 @@ os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY") # Now import mem0 - it will be instrumented by agentops -from mem0 import Memory, AsyncMemory +from mem0 import Memory, AsyncMemory # noqa E402 # Import agentops BEFORE mem0 to ensure proper instrumentation -import agentops +import agentops # noqa E402 def demonstrate_sync_memory(local_config, sample_messages, sample_preferences, user_id): """ Demonstrate synchronous Memory class operations. - + This function performs sequential memory operations including: - Adding conversation messages with metadata - Storing individual user preferences - Searching memories using natural language queries - Retrieving all memories for a user - Cleaning up memories after demonstration - + Args: local_config: Configuration dict for Memory initialization sample_messages: List of conversation messages to store sample_preferences: List of user preferences to store user_id: Unique identifier for the user - + Performance note: Sequential operations take longer as each operation must complete before the next one begins. """ agentops.start_trace("mem0_memory_example", tags=["mem0_memory_example"]) try: - # Initialize sync Memory with local configuration - memory = Memory.from_config(local_config) + memory = Memory.from_config(local_config) # Add conversation messages with metadata for categorization - result = memory.add( + result = memory.add( sample_messages, user_id=user_id, metadata={"category": "movie_preferences", "session": "demo"} ) - + # Add individual preferences sequentially for i, preference in enumerate(sample_preferences): result = memory.add(preference, user_id=user_id, metadata={"type": "preference", "index": i}) - + # 2. SEARCH operations - demonstrate natural language search capabilities search_queries = [ "What movies does the user like?", @@ -80,8 +78,8 @@ def demonstrate_sync_memory(local_config, sample_messages, sample_preferences, u ] for query in search_queries: - results = memory.search(query, user_id=user_id) - + results = memory.search(query, user_id=user_id) + if results and "results" in results: for j, result in enumerate(results["results"][:2]): # Show top 2 print(f"Result {j+1}: {result.get('memory', 'N/A')}") @@ -89,36 +87,36 @@ def demonstrate_sync_memory(local_config, sample_messages, sample_preferences, u print("No results found") # 3. GET_ALL operations - retrieve all memories for the user - all_memories = memory.get_all(user_id=user_id) + all_memories = memory.get_all(user_id=user_id) if all_memories and "results" in all_memories: print(f"Total memories: {len(all_memories['results'])}") # Cleanup - remove all memories for the user - delete_all_result = memory.delete_all(user_id=user_id) + delete_all_result = memory.delete_all(user_id=user_id) print(f"Delete all result: {delete_all_result}") agentops.end_trace(end_state="success") - except Exception as e: + except Exception: agentops.end_trace(end_state="error") async def demonstrate_async_memory(local_config, sample_messages, sample_preferences, user_id): """ Demonstrate asynchronous Memory class operations with concurrent execution. - + This function performs concurrent memory operations including: - Adding conversation messages asynchronously - Storing multiple preferences concurrently using asyncio.gather() - Performing parallel search operations - Retrieving all memories asynchronously - Cleaning up memories after demonstration - + Args: local_config: Configuration dict for AsyncMemory initialization sample_messages: List of conversation messages to store sample_preferences: List of user preferences to store user_id: Unique identifier for the user - + Performance benefit: Concurrent operations significantly reduce total execution time by running multiple memory operations in parallel. """ @@ -134,7 +132,6 @@ async def demonstrate_async_memory(local_config, sample_messages, sample_prefere sample_messages, user_id=user_id, metadata={"category": "async_movie_preferences", "session": "async_demo"} ) - # Add preferences concurrently using asyncio.gather() async def add_preference(preference, index): """Helper function to add a single preference asynchronously.""" @@ -181,9 +178,10 @@ async def search_memory(query): agentops.end_trace(end_state="success") - except Exception as e: + except Exception: agentops.end_trace(end_state="error") + # Configuration for local memory (Memory) # This configuration specifies the LLM provider and model settings local_config = { @@ -222,4 +220,4 @@ async def search_memory(query): # Execute both sync and async demonstrations demonstrate_sync_memory(local_config, sample_messages, sample_preferences, user_id) -asyncio.run(demonstrate_async_memory(local_config, sample_messages, sample_preferences, user_id)) \ No newline at end of file +asyncio.run(demonstrate_async_memory(local_config, sample_messages, sample_preferences, user_id)) From cdf0cdcbc2f614648b19099ce8b355f488639c19 Mon Sep 17 00:00:00 2001 From: Fenil Faldu <65482495+fenilfaldu@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:51:35 +0530 Subject: [PATCH 18/23] Delete examples/mem0/mem0_memory_basic_example.py --- examples/mem0/mem0_memory_basic_example.py | 142 --------------------- 1 file changed, 142 deletions(-) delete mode 100644 examples/mem0/mem0_memory_basic_example.py diff --git a/examples/mem0/mem0_memory_basic_example.py b/examples/mem0/mem0_memory_basic_example.py deleted file mode 100644 index 5318bfbad..000000000 --- a/examples/mem0/mem0_memory_basic_example.py +++ /dev/null @@ -1,142 +0,0 @@ -""" -Basic Memory Operations with Mem0: Local vs Cloud - -This example demonstrates the two primary ways to use Mem0 for memory management: -1. Memory class - For local, self-hosted memory storage -2. MemoryClient class - For cloud-based memory storage using Mem0's managed service - -Both approaches offer the same core functionality (add, search, get_all) but differ in: -- Storage location: Local vs Mem0's cloud infrastructure -- Setup requirements: Local configuration vs API key authentication -- Use cases: Development/self-hosted vs production/scalable applications - -Key features demonstrated: -- Adding individual memories with metadata -- Storing conversation history -- Searching memories using natural language -- Retrieving all memories for a user - -This example runs both approaches sequentially to showcase the similarities and differences. -""" - -import os -import asyncio -from dotenv import load_dotenv -from mem0 import Memory,MemoryClient -import agentops - -# Load environment variables from .env file -load_dotenv() - -# Set up API keys for AgentOps tracing -os.environ["AGENTOPS_API_KEY"] = os.getenv("AGENTOPS_API_KEY") -os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY") -api_key= os.getenv("MEM0_API_KEY") - -# Configuration for Memory with OpenAI as the LLM provider -# This configuration specifies which model to use and its parameters -openai_config = { - "llm": { - "provider": "openai", - "config": { - "model": "gpt-4o-mini", - "temperature": 0.1, # Low temperature for consistent outputs - "max_tokens": 2000, - "api_key": os.getenv("OPENAI_API_KEY"), - } - } -} - -# Sample conversation data demonstrating user preferences -# This shows how Mem0 can extract and remember information from natural dialogue -test_messages = [ - {"role": "user", "content": "I love science fiction movies and books."}, - {"role": "assistant", "content": "That's great! Sci-fi offers amazing worlds and concepts."}, - {"role": "user", "content": "I also prefer dark roast coffee over light roast."}, -] - -# Start AgentOps trace for monitoring and debugging -agentops.start_trace("mem0_memory_basic_example", tags=["mem0_memory_basic_example"]) -try: - # Initialize Memory instance with the OpenAI configuration - # This creates a local memory store that processes and stores memories on your infrastructure - m_sync = Memory.from_config(openai_config) - - # This demonstrates storing individual facts with metadata - result = m_sync.add( - [{"role": "user", "content": "I like to drink coffee in the morning and go for a walk."}], - user_id="alice_sync", - metadata={"category": "preferences", "type": "sync"} - ) - - # This shows how to store entire conversations that Mem0 will analyze for memorable information - result = m_sync.add( - test_messages, - user_id="alice_sync", - metadata={"category": "conversation", "type": "sync"} - ) - - # Demonstrates how to query stored memories with questions - search_result = m_sync.search("What does the user like to drink?",user_id="alice_sync") - - # Shows how to get a complete view of what Mem0 remembers about a user - all_memories = m_sync.get_all(user_id="alice_sync") - - - # Display all memories for inspection - if all_memories.get('results'): - for i, memory in enumerate(all_memories['results'], 1): - print(f"{i}. {memory.get('memory', 'N/A')}") - - # Successfully completed all operations - agentops.end_trace(end_state="success") - -except Exception as e: - # Log any errors that occur during execution - agentops.end_trace(end_state="error") - - -# Start AgentOps trace for monitoring and debugging -agentops.start_trace("mem0_memoryclient_basic_example", tags=["mem0_memoryclient_basic_example"]) -try: - # Initialize MemoryClient with API key for cloud access - # This connects to Mem0's cloud infrastructure for scalable memory management - m_sync = MemoryClient(api_key=api_key) - - # This demonstrates storing individual facts with metadata - result = m_sync.add( - [{"role": "user", "content": "I like to drink coffee in the morning and go for a walk."}], - user_id="alice_sync", - metadata={"category": "preferences", "type": "sync"} - ) - - # This shows how to store entire conversations that Mem0 will analyze for memorable information - result = m_sync.add( - test_messages, - user_id="alice_sync", - ) - filters = { - "AND": [ - { - "user_id": user_id - }, - ] - } - # Demonstrates how to query stored memories with questions - search_result = m_sync.search(version="v2",query="What does the user like to drink?",filters=filters) - print(f"Cloud search results: {search_result}") - - # Shows how to get all memories stored in Mem0's cloud for a user - all_memories = m_sync.get_all(version="v2",filters=filters,page_size=1) - print(f"Total cloud memories: {len(all_memories.get('results', []))}") - - # Display all memories for inspection - if all_memories.get('results'): - for i, memory in enumerate(all_memories['results'], 1): - print(f"{i}. {memory.get('memory', 'N/A')}") - - # Successfully completed all operations - agentops.end_trace(end_state="success") -except Exception as e: - # Log any errors that occur during execution - agentops.end_trace(end_state="error") From fc27638a94fd54da8104cfb27b2b4a909b55afe9 Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Fri, 13 Jun 2025 20:52:55 +0530 Subject: [PATCH 19/23] ruff checks again --- examples/mem0/mem0_memoryclient_example.py | 30 ++++++++++------------ 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/examples/mem0/mem0_memoryclient_example.py b/examples/mem0/mem0_memoryclient_example.py index c99c52986..2928fa99e 100644 --- a/examples/mem0/mem0_memoryclient_example.py +++ b/examples/mem0/mem0_memoryclient_example.py @@ -27,16 +27,16 @@ mem0_api_key = os.getenv("MEM0_API_KEY") # Import agentops BEFORE mem0 to ensure proper instrumentation -import agentops +import agentops # noqa E402 # Now import mem0 - it will be instrumented by agentops -from mem0 import MemoryClient, AsyncMemoryClient +from mem0 import MemoryClient, AsyncMemoryClient # noqa E402 def demonstrate_sync_memory_client(sample_messages, sample_preferences, user_id): """ Demonstrate synchronous MemoryClient operations with cloud storage. - + This function performs sequential cloud memory operations including: - Initializing cloud-based memory client with API authentication - Adding conversation messages to cloud storage @@ -44,21 +44,20 @@ def demonstrate_sync_memory_client(sample_messages, sample_preferences, user_id) - Searching memories using natural language - Retrieving memories with filters - Cleaning up cloud memories - + Args: sample_messages: List of conversation messages to store sample_preferences: List of user preferences to store user_id: Unique identifier for the user - + Cloud benefit: All memory operations are handled by Mem0's infrastructure, providing scalability and persistence without local storage management. """ - agentops.start_trace("mem0_memoryclient_example",tags=["mem0_memoryclient_example"]) + agentops.start_trace("mem0_memoryclient_example", tags=["mem0_memoryclient_example"]) try: # Initialize sync MemoryClient with API key for cloud access client = MemoryClient(api_key=mem0_api_key) - # Add conversation to cloud storage with metadata result = client.add( sample_messages, user_id=user_id, metadata={"category": "cloud_movie_preferences", "session": "cloud_demo"} @@ -68,7 +67,6 @@ def demonstrate_sync_memory_client(sample_messages, sample_preferences, user_id) # Add preferences sequentially to cloud for i, preference in enumerate(sample_preferences[:3]): # Limit for demo result = client.add(preference, user_id=user_id, metadata={"type": "cloud_preference", "index": i}) - # 2. SEARCH operations - leverage cloud search capabilities search_result = client.search("What are the user's movie preferences?", user_id=user_id) @@ -83,35 +81,35 @@ def demonstrate_sync_memory_client(sample_messages, sample_preferences, user_id) delete_all_result = client.delete_all(user_id=user_id) print(f"Delete all result: {delete_all_result}") agentops.end_trace(end_state="success") - except Exception as e: + except Exception: agentops.end_trace(end_state="error") async def demonstrate_async_memory_client(sample_messages, sample_preferences, user_id): """ Demonstrate asynchronous MemoryClient operations with concurrent cloud access. - + This function performs concurrent cloud memory operations including: - Initializing async cloud-based memory client - Adding multiple memories concurrently using asyncio.gather() - Performing parallel search operations across cloud storage - Retrieving filtered memories asynchronously - Cleaning up cloud memories efficiently - + Args: sample_messages: List of conversation messages to store sample_preferences: List of user preferences to store user_id: Unique identifier for the user - + Performance benefit: Async operations allow multiple cloud API calls to execute concurrently, significantly reducing total execution time compared to sequential calls. This is especially beneficial when dealing with network I/O to cloud services. """ - agentops.start_trace("mem0_memoryclient_example",tags=["mem0_memoryclient_example"]) + agentops.start_trace("mem0_memoryclient_example", tags=["mem0_memoryclient_example"]) try: # Initialize async MemoryClient for concurrent cloud operations async_client = AsyncMemoryClient(api_key=mem0_api_key) - + # Add conversation and preferences concurrently to cloud add_conversation_task = async_client.add( sample_messages, user_id=user_id, metadata={"category": "async_cloud_movies", "session": "async_cloud_demo"} @@ -151,7 +149,7 @@ async def demonstrate_async_memory_client(sample_messages, sample_preferences, u agentops.end_trace(end_state="success") - except Exception as e: + except Exception: agentops.end_trace(end_state="error") @@ -183,4 +181,4 @@ async def demonstrate_async_memory_client(sample_messages, sample_preferences, u # Execute both sync and async demonstrations # Note: The async version typically completes faster due to concurrent operations demonstrate_sync_memory_client(sample_messages, sample_preferences, user_id) -asyncio.run(demonstrate_async_memory_client(sample_messages, sample_preferences, user_id)) \ No newline at end of file +asyncio.run(demonstrate_async_memory_client(sample_messages, sample_preferences, user_id)) From ab374369383ead2b4c9798b098620cb57d71877b Mon Sep 17 00:00:00 2001 From: fenilfaldu Date: Fri, 13 Jun 2025 20:54:49 +0530 Subject: [PATCH 20/23] Format mem0_memory_basic_example.py --- examples/mem0/mem0_memory_basic_example.py | 56 ++++++++++------------ 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/examples/mem0/mem0_memory_basic_example.py b/examples/mem0/mem0_memory_basic_example.py index 5318bfbad..69e7f5f7d 100644 --- a/examples/mem0/mem0_memory_basic_example.py +++ b/examples/mem0/mem0_memory_basic_example.py @@ -20,9 +20,8 @@ """ import os -import asyncio from dotenv import load_dotenv -from mem0 import Memory,MemoryClient +from mem0 import Memory, MemoryClient import agentops # Load environment variables from .env file @@ -31,7 +30,7 @@ # Set up API keys for AgentOps tracing os.environ["AGENTOPS_API_KEY"] = os.getenv("AGENTOPS_API_KEY") os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY") -api_key= os.getenv("MEM0_API_KEY") +api_key = os.getenv("MEM0_API_KEY") # Configuration for Memory with OpenAI as the LLM provider # This configuration specifies which model to use and its parameters @@ -43,7 +42,7 @@ "temperature": 0.1, # Low temperature for consistent outputs "max_tokens": 2000, "api_key": os.getenv("OPENAI_API_KEY"), - } + }, } } @@ -64,34 +63,29 @@ # This demonstrates storing individual facts with metadata result = m_sync.add( - [{"role": "user", "content": "I like to drink coffee in the morning and go for a walk."}], - user_id="alice_sync", - metadata={"category": "preferences", "type": "sync"} + [{"role": "user", "content": "I like to drink coffee in the morning and go for a walk."}], + user_id="alice_sync", + metadata={"category": "preferences", "type": "sync"}, ) # This shows how to store entire conversations that Mem0 will analyze for memorable information - result = m_sync.add( - test_messages, - user_id="alice_sync", - metadata={"category": "conversation", "type": "sync"} - ) + result = m_sync.add(test_messages, user_id="alice_sync", metadata={"category": "conversation", "type": "sync"}) # Demonstrates how to query stored memories with questions - search_result = m_sync.search("What does the user like to drink?",user_id="alice_sync") + search_result = m_sync.search("What does the user like to drink?", user_id="alice_sync") # Shows how to get a complete view of what Mem0 remembers about a user all_memories = m_sync.get_all(user_id="alice_sync") - # Display all memories for inspection - if all_memories.get('results'): - for i, memory in enumerate(all_memories['results'], 1): + if all_memories.get("results"): + for i, memory in enumerate(all_memories["results"], 1): print(f"{i}. {memory.get('memory', 'N/A')}") - + # Successfully completed all operations agentops.end_trace(end_state="success") -except Exception as e: +except Exception: # Log any errors that occur during execution agentops.end_trace(end_state="error") @@ -105,9 +99,9 @@ # This demonstrates storing individual facts with metadata result = m_sync.add( - [{"role": "user", "content": "I like to drink coffee in the morning and go for a walk."}], - user_id="alice_sync", - metadata={"category": "preferences", "type": "sync"} + [{"role": "user", "content": "I like to drink coffee in the morning and go for a walk."}], + user_id="alice_sync", + metadata={"category": "preferences", "type": "sync"}, ) # This shows how to store entire conversations that Mem0 will analyze for memorable information @@ -117,26 +111,24 @@ ) filters = { "AND": [ - { - "user_id": user_id - }, - ] + {"user_id": user_id}, + ] } # Demonstrates how to query stored memories with questions - search_result = m_sync.search(version="v2",query="What does the user like to drink?",filters=filters) + search_result = m_sync.search(version="v2", query="What does the user like to drink?", filters=filters) print(f"Cloud search results: {search_result}") # Shows how to get all memories stored in Mem0's cloud for a user - all_memories = m_sync.get_all(version="v2",filters=filters,page_size=1) + all_memories = m_sync.get_all(version="v2", filters=filters, page_size=1) print(f"Total cloud memories: {len(all_memories.get('results', []))}") - + # Display all memories for inspection - if all_memories.get('results'): - for i, memory in enumerate(all_memories['results'], 1): + if all_memories.get("results"): + for i, memory in enumerate(all_memories["results"], 1): print(f"{i}. {memory.get('memory', 'N/A')}") - + # Successfully completed all operations agentops.end_trace(end_state="success") -except Exception as e: +except Exception: # Log any errors that occur during execution agentops.end_trace(end_state="error") From 2cf12a11857457581d25b3d13fe20f0f2687eef3 Mon Sep 17 00:00:00 2001 From: Fenil Faldu <65482495+fenilfaldu@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:56:49 +0530 Subject: [PATCH 21/23] code cleanup --- examples/mem0/mem0_memory_basic_example.py | 135 --------------------- 1 file changed, 135 deletions(-) delete mode 100644 examples/mem0/mem0_memory_basic_example.py diff --git a/examples/mem0/mem0_memory_basic_example.py b/examples/mem0/mem0_memory_basic_example.py deleted file mode 100644 index 6fcbb6a42..000000000 --- a/examples/mem0/mem0_memory_basic_example.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -Basic Memory Operations with Mem0: Local vs Cloud - -This example demonstrates the two primary ways to use Mem0 for memory management: -1. Memory class - For local, self-hosted memory storage -2. MemoryClient class - For cloud-based memory storage using Mem0's managed service - -Both approaches offer the same core functionality (add, search, get_all) but differ in: -- Storage location: Local vs Mem0's cloud infrastructure -- Setup requirements: Local configuration vs API key authentication -- Use cases: Development/self-hosted vs production/scalable applications - -Key features demonstrated: -- Adding individual memories with metadata -- Storing conversation history -- Searching memories using natural language -- Retrieving all memories for a user - -This example runs both approaches sequentially to showcase the similarities and differences. -""" - -import os -from dotenv import load_dotenv -from mem0 import Memory, MemoryClient -import agentops - -# Load environment variables from .env file -load_dotenv() - -# Set up API keys for AgentOps tracing -os.environ["AGENTOPS_API_KEY"] = os.getenv("AGENTOPS_API_KEY") -os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY") -api_key = os.getenv("MEM0_API_KEY") - -# Configuration for Memory with OpenAI as the LLM provider -# This configuration specifies which model to use and its parameters -openai_config = { - "llm": { - "provider": "openai", - "config": { - "model": "gpt-4o-mini", - "temperature": 0.1, # Low temperature for consistent outputs - "max_tokens": 2000, - "api_key": os.getenv("OPENAI_API_KEY"), - }, - } -} - -# Sample conversation data demonstrating user preferences -# This shows how Mem0 can extract and remember information from natural dialogue -test_messages = [ - {"role": "user", "content": "I love science fiction movies and books."}, - {"role": "assistant", "content": "That's great! Sci-fi offers amazing worlds and concepts."}, - {"role": "user", "content": "I also prefer dark roast coffee over light roast."}, -] - -# Start AgentOps trace for monitoring and debugging -agentops.start_trace("mem0_memory_basic_example", tags=["mem0_memory_basic_example"]) -try: - # Initialize Memory instance with the OpenAI configuration - # This creates a local memory store that processes and stores memories on your infrastructure - m_sync = Memory.from_config(openai_config) - - # This demonstrates storing individual facts with metadata - result = m_sync.add( - [{"role": "user", "content": "I like to drink coffee in the morning and go for a walk."}], - user_id="alice_sync", - metadata={"category": "preferences", "type": "sync"}, - ) - - # This shows how to store entire conversations that Mem0 will analyze for memorable information - result = m_sync.add(test_messages, user_id="alice_sync", metadata={"category": "conversation", "type": "sync"}) - - # Demonstrates how to query stored memories with questions - search_result = m_sync.search("What does the user like to drink?", user_id="alice_sync") - - # Shows how to get a complete view of what Mem0 remembers about a user - all_memories = m_sync.get_all(user_id="alice_sync") - - # Display all memories for inspection - if all_memories.get("results"): - for i, memory in enumerate(all_memories["results"], 1): - print(f"{i}. {memory.get('memory', 'N/A')}") - - # Successfully completed all operations - agentops.end_trace(end_state="success") - -except Exception: - # Log any errors that occur during execution - agentops.end_trace(end_state="error") - - -# Start AgentOps trace for monitoring and debugging -agentops.start_trace("mem0_memoryclient_basic_example", tags=["mem0_memoryclient_basic_example"]) -try: - # Initialize MemoryClient with API key for cloud access - # This connects to Mem0's cloud infrastructure for scalable memory management - m_sync = MemoryClient(api_key=api_key) - - # This demonstrates storing individual facts with metadata - result = m_sync.add( - [{"role": "user", "content": "I like to drink coffee in the morning and go for a walk."}], - user_id="alice_sync", - metadata={"category": "preferences", "type": "sync"}, - ) - - # This shows how to store entire conversations that Mem0 will analyze for memorable information - result = m_sync.add( - test_messages, - user_id="alice_sync", - ) - filters = { - "AND": [ - {"user_id": user_id}, - ] - } - # Demonstrates how to query stored memories with questions - search_result = m_sync.search(version="v2", query="What does the user like to drink?", filters=filters) - print(f"Cloud search results: {search_result}") - - # Shows how to get all memories stored in Mem0's cloud for a user - all_memories = m_sync.get_all(version="v2", filters=filters, page_size=1) - print(f"Total cloud memories: {len(all_memories.get('results', []))}") - - # Display all memories for inspection - if all_memories.get("results"): - for i, memory in enumerate(all_memories["results"], 1): - print(f"{i}. {memory.get('memory', 'N/A')}") - - # Successfully completed all operations - agentops.end_trace(end_state="success") -except Exception: - # Log any errors that occur during execution - agentops.end_trace(end_state="error") - From 3c8f0a84d60d09d91b82d72b28f6c3fc3dcdf471 Mon Sep 17 00:00:00 2001 From: Pratyush Shukla Date: Fri, 13 Jun 2025 23:18:44 +0530 Subject: [PATCH 22/23] add "Example" to Title --- examples/generate_documentation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/generate_documentation.py b/examples/generate_documentation.py index 255f4d275..532bfd28c 100644 --- a/examples/generate_documentation.py +++ b/examples/generate_documentation.py @@ -104,7 +104,7 @@ def generate_mdx_content(notebook_path, processed_content, frontmatter=None): if not frontmatter: # Generate new frontmatter folder_name = Path(notebook_path).parent.name - title = folder_name.replace("_", " ").title() + title = f"{folder_name.replace('_', ' ').title()} Example" # Extract description from first heading or use default description = f"{title} example using AgentOps" From c6837ad0f6e2e564392099c323d7423ceb68a9b9 Mon Sep 17 00:00:00 2001 From: Pratyush Shukla Date: Fri, 13 Jun 2025 23:19:03 +0530 Subject: [PATCH 23/23] some doc changes --- docs/v2/examples/mem0.mdx | 2 +- docs/v2/integrations/mem0.mdx | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/v2/examples/mem0.mdx b/docs/v2/examples/mem0.mdx index 0ce76d2dc..1dc8dc781 100644 --- a/docs/v2/examples/mem0.mdx +++ b/docs/v2/examples/mem0.mdx @@ -1,5 +1,5 @@ --- -title: 'Mem0' +title: 'Mem0 Example' description: 'Memory Operations with Mem0' --- {/* SOURCE_FILE: examples/mem0/mem0_memory_example.ipynb */} diff --git a/docs/v2/integrations/mem0.mdx b/docs/v2/integrations/mem0.mdx index 873b69a5d..562ed3b49 100644 --- a/docs/v2/integrations/mem0.mdx +++ b/docs/v2/integrations/mem0.mdx @@ -47,10 +47,9 @@ Load environment variables and set up API keys. The MEM0_API_KEY is only require ## Tracking Memory Operations -#### Local Memory with AgentOps -AgentOps automatically instruments Local Memory Mem0 methods: -```python + +```python Local Memory import agentops from mem0 import Memory @@ -89,10 +88,7 @@ except Exception as e: agentops.end_trace(end_state="error") ``` -#### Cloud Memory with agentops -AgentOps automatically instruments Cloud Memory Mem0 methods: - -```python +```python Cloud Memory import agentops from mem0 import MemoryClient @@ -125,6 +121,8 @@ try: except Exception as e: agentops.end_trace(end_state="error") ``` + + ## What You'll See in AgentOps When using Mem0 with AgentOps, your dashboard will show: @@ -139,11 +137,11 @@ When using Mem0 with AgentOps, your dashboard will show: ## Examples - + Simple example showing memory storage and retrieval with AgentOps tracking - + Track concurrent memory operations with async/await patterns