lmnr 0.4.53.dev0__py3-none-any.whl → 0.7.26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. lmnr/__init__.py +32 -11
  2. lmnr/cli/__init__.py +270 -0
  3. lmnr/cli/datasets.py +371 -0
  4. lmnr/cli/evals.py +111 -0
  5. lmnr/cli/rules.py +42 -0
  6. lmnr/opentelemetry_lib/__init__.py +70 -0
  7. lmnr/opentelemetry_lib/decorators/__init__.py +337 -0
  8. lmnr/opentelemetry_lib/litellm/__init__.py +685 -0
  9. lmnr/opentelemetry_lib/litellm/utils.py +100 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
  16. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
  17. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
  18. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
  19. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
  20. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
  21. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
  22. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
  23. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +599 -0
  24. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py +9 -0
  25. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
  26. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +330 -0
  27. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
  28. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
  29. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
  30. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
  31. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
  32. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
  33. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
  34. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
  35. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
  36. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +121 -0
  37. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/utils.py +60 -0
  38. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
  39. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
  40. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
  41. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
  42. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
  43. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
  44. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
  45. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  46. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
  47. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
  48. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
  49. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
  50. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
  51. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
  52. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
  53. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
  54. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
  55. lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
  56. lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +191 -0
  57. lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
  58. lmnr/opentelemetry_lib/tracing/__init__.py +263 -0
  59. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +516 -0
  60. lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +21 -8
  61. lmnr/opentelemetry_lib/tracing/context.py +200 -0
  62. lmnr/opentelemetry_lib/tracing/exporter.py +153 -0
  63. lmnr/opentelemetry_lib/tracing/instruments.py +140 -0
  64. lmnr/opentelemetry_lib/tracing/processor.py +193 -0
  65. lmnr/opentelemetry_lib/tracing/span.py +398 -0
  66. lmnr/opentelemetry_lib/tracing/tracer.py +57 -0
  67. lmnr/opentelemetry_lib/tracing/utils.py +62 -0
  68. lmnr/opentelemetry_lib/utils/package_check.py +18 -0
  69. lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
  70. lmnr/sdk/browser/__init__.py +0 -0
  71. lmnr/sdk/browser/background_send_events.py +158 -0
  72. lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
  73. lmnr/sdk/browser/browser_use_otel.py +142 -0
  74. lmnr/sdk/browser/bubus_otel.py +71 -0
  75. lmnr/sdk/browser/cdp_utils.py +518 -0
  76. lmnr/sdk/browser/inject_script.js +514 -0
  77. lmnr/sdk/browser/patchright_otel.py +151 -0
  78. lmnr/sdk/browser/playwright_otel.py +322 -0
  79. lmnr/sdk/browser/pw_utils.py +363 -0
  80. lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
  81. lmnr/sdk/browser/utils.py +70 -0
  82. lmnr/sdk/client/asynchronous/async_client.py +180 -0
  83. lmnr/sdk/client/asynchronous/resources/__init__.py +6 -0
  84. lmnr/sdk/client/asynchronous/resources/base.py +32 -0
  85. lmnr/sdk/client/asynchronous/resources/browser_events.py +41 -0
  86. lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
  87. lmnr/sdk/client/asynchronous/resources/evals.py +266 -0
  88. lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
  89. lmnr/sdk/client/asynchronous/resources/tags.py +83 -0
  90. lmnr/sdk/client/synchronous/resources/__init__.py +6 -0
  91. lmnr/sdk/client/synchronous/resources/base.py +32 -0
  92. lmnr/sdk/client/synchronous/resources/browser_events.py +40 -0
  93. lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
  94. lmnr/sdk/client/synchronous/resources/evals.py +263 -0
  95. lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
  96. lmnr/sdk/client/synchronous/resources/tags.py +83 -0
  97. lmnr/sdk/client/synchronous/sync_client.py +191 -0
  98. lmnr/sdk/datasets/__init__.py +94 -0
  99. lmnr/sdk/datasets/file_utils.py +91 -0
  100. lmnr/sdk/decorators.py +163 -26
  101. lmnr/sdk/eval_control.py +3 -2
  102. lmnr/sdk/evaluations.py +403 -191
  103. lmnr/sdk/laminar.py +1080 -549
  104. lmnr/sdk/log.py +7 -2
  105. lmnr/sdk/types.py +246 -134
  106. lmnr/sdk/utils.py +151 -7
  107. lmnr/version.py +46 -0
  108. {lmnr-0.4.53.dev0.dist-info → lmnr-0.7.26.dist-info}/METADATA +152 -106
  109. lmnr-0.7.26.dist-info/RECORD +116 -0
  110. lmnr-0.7.26.dist-info/WHEEL +4 -0
  111. lmnr-0.7.26.dist-info/entry_points.txt +3 -0
  112. lmnr/cli.py +0 -101
  113. lmnr/openllmetry_sdk/.python-version +0 -1
  114. lmnr/openllmetry_sdk/__init__.py +0 -72
  115. lmnr/openllmetry_sdk/config/__init__.py +0 -9
  116. lmnr/openllmetry_sdk/decorators/base.py +0 -185
  117. lmnr/openllmetry_sdk/instruments.py +0 -38
  118. lmnr/openllmetry_sdk/tracing/__init__.py +0 -1
  119. lmnr/openllmetry_sdk/tracing/content_allow_list.py +0 -24
  120. lmnr/openllmetry_sdk/tracing/context_manager.py +0 -13
  121. lmnr/openllmetry_sdk/tracing/tracing.py +0 -884
  122. lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py +0 -61
  123. lmnr/openllmetry_sdk/utils/package_check.py +0 -7
  124. lmnr/openllmetry_sdk/version.py +0 -1
  125. lmnr/sdk/datasets.py +0 -55
  126. lmnr-0.4.53.dev0.dist-info/LICENSE +0 -75
  127. lmnr-0.4.53.dev0.dist-info/RECORD +0 -33
  128. lmnr-0.4.53.dev0.dist-info/WHEEL +0 -4
  129. lmnr-0.4.53.dev0.dist-info/entry_points.txt +0 -3
  130. /lmnr/{openllmetry_sdk → opentelemetry_lib}/.flake8 +0 -0
  131. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/__init__.py +0 -0
  132. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
  133. /lmnr/{openllmetry_sdk/decorators/__init__.py → py.typed} +0 -0
lmnr/cli/evals.py ADDED
@@ -0,0 +1,111 @@
1
+ from argparse import Namespace
2
+
3
+ import glob
4
+ import importlib.util
5
+ import json
6
+ import os
7
+ import re
8
+ import sys
9
+
10
+ from lmnr.sdk.evaluations import Evaluation
11
+ from lmnr.sdk.eval_control import PREPARE_ONLY, EVALUATION_INSTANCES
12
+ from lmnr.sdk.log import get_default_logger
13
+
14
+ LOG = get_default_logger(__name__)
15
+ EVAL_DIR = "evals"
16
+ DEFAULT_DATASET_PULL_BATCH_SIZE = 100
17
+ DEFAULT_DATASET_PUSH_BATCH_SIZE = 100
18
+
19
+
20
+ def log_evaluation_instance_not_found() -> None:
21
+ LOG.warning(
22
+ "Evaluation instance not found. "
23
+ "`evaluate` must be called at the top level of the file, "
24
+ "not inside a function when running evaluations from the CLI."
25
+ )
26
+
27
+
28
+ async def run_evaluation(args: Namespace) -> None:
29
+ sys.path.append(os.getcwd())
30
+
31
+ if len(args.file) == 0:
32
+ files = [
33
+ os.path.join(EVAL_DIR, f)
34
+ for f in os.listdir(EVAL_DIR)
35
+ if re.match(r".*_eval\.py$", f) or re.match(r"eval_.*\.py$", f)
36
+ ]
37
+ if len(files) == 0:
38
+ LOG.error("No evaluation files found in `evals` directory")
39
+ LOG.info(
40
+ "Eval files must be located in the `evals` directory and must be named *_eval.py or eval_*.py"
41
+ )
42
+ return
43
+ files.sort()
44
+ LOG.info(f"Located {len(files)} evaluation files in {EVAL_DIR}")
45
+
46
+ else:
47
+ files = []
48
+ for pattern in args.file:
49
+ matches = glob.glob(pattern)
50
+ if matches:
51
+ files.extend(matches)
52
+ else:
53
+ # If no matches found, treat as literal filename
54
+ files.append(pattern)
55
+
56
+ prep_token = PREPARE_ONLY.set(True)
57
+ scores = []
58
+ try:
59
+ for file in files:
60
+ # Reset EVALUATION_INSTANCES before loading each file
61
+ EVALUATION_INSTANCES.set([])
62
+
63
+ LOG.info(f"Running evaluation from {file}")
64
+ file = os.path.abspath(file)
65
+ name = "user_module" + file
66
+
67
+ spec = importlib.util.spec_from_file_location(name, file)
68
+ if spec is None or spec.loader is None:
69
+ LOG.error(f"Could not load module specification from {file}")
70
+ if args.continue_on_error:
71
+ continue
72
+ return
73
+ mod = importlib.util.module_from_spec(spec)
74
+ sys.modules[name] = mod
75
+
76
+ spec.loader.exec_module(mod)
77
+ evaluations = []
78
+ try:
79
+ evaluations: list[Evaluation] | None = EVALUATION_INSTANCES.get()
80
+ if evaluations is None:
81
+ raise LookupError()
82
+ # may be raised by `get()` or manually by us above
83
+ except LookupError:
84
+ log_evaluation_instance_not_found()
85
+ if args.continue_on_error:
86
+ continue
87
+ return
88
+
89
+ LOG.info(f"Loaded {len(evaluations)} evaluations from {file}")
90
+
91
+ for evaluation in evaluations:
92
+ try:
93
+ eval_result = await evaluation.run()
94
+ scores.append(
95
+ {
96
+ "file": file,
97
+ "scores": eval_result["average_scores"],
98
+ "evaluation_id": str(eval_result["evaluation_id"]),
99
+ "url": eval_result["url"],
100
+ }
101
+ )
102
+ except Exception as e:
103
+ LOG.error(f"Error running evaluation: {e}")
104
+ if not args.continue_on_error:
105
+ raise
106
+
107
+ if args.output_file:
108
+ with open(args.output_file, "w") as f:
109
+ json.dump(scores, f, indent=2)
110
+ finally:
111
+ PREPARE_ONLY.reset(prep_token)
lmnr/cli/rules.py ADDED
@@ -0,0 +1,42 @@
1
+ from pathlib import Path
2
+ import urllib.request
3
+ import urllib.error
4
+ import sys
5
+
6
+
7
+ from lmnr.sdk.log import get_default_logger
8
+
9
+ LOG = get_default_logger(__name__)
10
+
11
+
12
+ def add_cursor_rules() -> None:
13
+ """Download laminar.mdc file from a hardcoded public URL and save it to .cursor/rules/laminar.mdc"""
14
+ # Hardcoded URL for the laminar.mdc file
15
+ url = "https://raw.githubusercontent.com/lmnr-ai/lmnr/dev/rules/laminar.mdc"
16
+
17
+ # Create .cursor/rules directory if it doesn't exist
18
+ rules_dir = Path(".cursor/rules")
19
+ rules_dir.mkdir(parents=True, exist_ok=True)
20
+
21
+ # Define the target file path
22
+ target_file = rules_dir / "laminar.mdc"
23
+
24
+ try:
25
+ LOG.info(f"Downloading laminar.mdc from {url}")
26
+
27
+ # Download the file
28
+ with urllib.request.urlopen(url) as response:
29
+ content = response.read()
30
+
31
+ # Write the content to the target file (this will overwrite if it exists)
32
+ with open(target_file, "wb") as f:
33
+ f.write(content)
34
+
35
+ LOG.info(f"Successfully downloaded laminar.mdc to {target_file}")
36
+
37
+ except urllib.error.URLError as e:
38
+ LOG.error(f"Failed to download file from {url}: {e}")
39
+ sys.exit(1)
40
+ except Exception as e:
41
+ LOG.error(f"Unexpected error: {e}")
42
+ sys.exit(1)
@@ -0,0 +1,70 @@
1
+ import logging
2
+ import sys
3
+
4
+ from opentelemetry.sdk.trace.export import SpanExporter
5
+ from opentelemetry.sdk.resources import SERVICE_NAME
6
+
7
+ from lmnr.opentelemetry_lib.tracing.instruments import Instruments
8
+ from lmnr.opentelemetry_lib.tracing import TracerWrapper
9
+ from lmnr.sdk.types import SessionRecordingOptions
10
+
11
+
12
+ class TracerManager:
13
+ __tracer_wrapper: TracerWrapper
14
+
15
+ @staticmethod
16
+ def init(
17
+ app_name: str | None = sys.argv[0],
18
+ disable_batch=False,
19
+ exporter: SpanExporter | None = None,
20
+ resource_attributes: dict = {},
21
+ instruments: set[Instruments] | None = None,
22
+ block_instruments: set[Instruments] | None = None,
23
+ base_url: str = "https://api.lmnr.ai",
24
+ port: int = 8443,
25
+ http_port: int = 443,
26
+ project_api_key: str | None = None,
27
+ max_export_batch_size: int | None = None,
28
+ force_http: bool = False,
29
+ timeout_seconds: int = 30,
30
+ set_global_tracer_provider: bool = True,
31
+ otel_logger_level: int = logging.ERROR,
32
+ session_recording_options: SessionRecordingOptions | None = None,
33
+ ) -> None:
34
+ enable_content_tracing = True
35
+
36
+ # Tracer init
37
+ resource_attributes.update({SERVICE_NAME: app_name})
38
+ TracerWrapper.set_static_params(resource_attributes, enable_content_tracing)
39
+ TracerManager.__tracer_wrapper = TracerWrapper(
40
+ disable_batch=disable_batch,
41
+ exporter=exporter,
42
+ instruments=instruments,
43
+ block_instruments=block_instruments,
44
+ base_url=base_url,
45
+ port=port,
46
+ http_port=http_port,
47
+ project_api_key=project_api_key,
48
+ max_export_batch_size=max_export_batch_size,
49
+ force_http=force_http,
50
+ timeout_seconds=timeout_seconds,
51
+ set_global_tracer_provider=set_global_tracer_provider,
52
+ otel_logger_level=otel_logger_level,
53
+ session_recording_options=session_recording_options,
54
+ )
55
+
56
+ @staticmethod
57
+ def flush() -> bool:
58
+ if not hasattr(TracerManager, "_TracerManager__tracer_wrapper"):
59
+ return False
60
+ return TracerManager.__tracer_wrapper.flush()
61
+
62
+ @staticmethod
63
+ def shutdown():
64
+ TracerManager.__tracer_wrapper.shutdown()
65
+
66
+ @staticmethod
67
+ def force_reinit_processor():
68
+ if not hasattr(TracerManager, "_TracerManager__tracer_wrapper"):
69
+ return False
70
+ return TracerManager.__tracer_wrapper.force_reinit_processor()
@@ -0,0 +1,337 @@
1
+ from functools import wraps
2
+ import types
3
+ from typing import Any, AsyncGenerator, Callable, Generator, Literal, TypeVar
4
+
5
+ from opentelemetry import context as context_api
6
+ from opentelemetry.trace import Span, Status, StatusCode
7
+
8
+ from lmnr.opentelemetry_lib.tracing.context import (
9
+ CONTEXT_METADATA_KEY,
10
+ attach_context,
11
+ detach_context,
12
+ get_event_attributes_from_context,
13
+ )
14
+ from lmnr.opentelemetry_lib.tracing.span import LaminarSpan
15
+ from lmnr.opentelemetry_lib.tracing.utils import set_association_props_in_context
16
+ from lmnr.sdk.utils import get_input_from_func_args, is_method
17
+ from lmnr.opentelemetry_lib.tracing.tracer import get_tracer_with_context
18
+ from lmnr.opentelemetry_lib.tracing.attributes import (
19
+ ASSOCIATION_PROPERTIES,
20
+ METADATA,
21
+ SPAN_TYPE,
22
+ )
23
+ from lmnr.opentelemetry_lib.tracing import TracerWrapper
24
+ from lmnr.sdk.log import get_default_logger
25
+ from lmnr.sdk.utils import is_otel_attribute_value_type, json_dumps
26
+
27
+ logger = get_default_logger(__name__)
28
+
29
+ F = TypeVar("F", bound=Callable[..., Any])
30
+
31
+
32
+ def _setup_span(
33
+ span_name: str,
34
+ span_type: str,
35
+ association_properties: dict[str, Any] | None,
36
+ preserve_global_context: bool = False,
37
+ metadata: dict[str, Any] | None = None,
38
+ ):
39
+ """Set up a span with the given name, type, and association properties."""
40
+ with get_tracer_with_context() as (tracer, isolated_context):
41
+ # Create span in isolated context
42
+ span = tracer.start_span(
43
+ span_name,
44
+ context=isolated_context if not preserve_global_context else None,
45
+ attributes={SPAN_TYPE: span_type},
46
+ )
47
+
48
+ ctx_metadata = context_api.get_value(CONTEXT_METADATA_KEY, isolated_context)
49
+ merged_metadata = {
50
+ **(ctx_metadata or {}),
51
+ **(metadata or {}),
52
+ }
53
+ for key, value in merged_metadata.items():
54
+ span.set_attribute(
55
+ f"{ASSOCIATION_PROPERTIES}.{METADATA}.{key}",
56
+ (value if is_otel_attribute_value_type(value) else json_dumps(value)),
57
+ )
58
+
59
+ if association_properties is not None:
60
+ for key, value in association_properties.items():
61
+ span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{key}", value)
62
+
63
+ return span
64
+
65
+
66
+ def _process_input(
67
+ span: Span,
68
+ fn: Callable,
69
+ args: tuple,
70
+ kwargs: dict,
71
+ ignore_input: bool,
72
+ ignore_inputs: list[str] | None,
73
+ input_formatter: Callable[..., str] | None,
74
+ ):
75
+ """Process and set input attributes on the span."""
76
+ if ignore_input:
77
+ return
78
+
79
+ try:
80
+ if input_formatter is not None:
81
+ inp = input_formatter(*args, **kwargs)
82
+ else:
83
+ inp = get_input_from_func_args(
84
+ fn,
85
+ is_method=is_method(fn),
86
+ func_args=args,
87
+ func_kwargs=kwargs,
88
+ ignore_inputs=ignore_inputs,
89
+ )
90
+
91
+ if not isinstance(span, LaminarSpan):
92
+ span = LaminarSpan(span)
93
+ span.set_input(inp)
94
+ except Exception:
95
+ msg = "Failed to process input, ignoring"
96
+ if input_formatter is not None:
97
+ # Only warn the user if they provided an input formatter
98
+ # because it's their responsibility to make sure it works.
99
+ logger.warning(msg, exc_info=True)
100
+ else:
101
+ logger.debug(msg, exc_info=True)
102
+ pass
103
+
104
+
105
+ def _process_output(
106
+ span: Span,
107
+ result: Any,
108
+ ignore_output: bool,
109
+ output_formatter: Callable[..., str] | None,
110
+ ):
111
+ """Process and set output attributes on the span."""
112
+ if ignore_output:
113
+ return
114
+
115
+ try:
116
+ if output_formatter is not None:
117
+ output = output_formatter(result)
118
+ else:
119
+ output = result
120
+
121
+ if not isinstance(span, LaminarSpan):
122
+ span = LaminarSpan(span)
123
+ span.set_output(output)
124
+ except Exception:
125
+ msg = "Failed to process output, ignoring"
126
+ if output_formatter is not None:
127
+ # Only warn the user if they provided an output formatter
128
+ # because it's their responsibility to make sure it works.
129
+ logger.warning(msg, exc_info=True)
130
+ else:
131
+ logger.debug(msg, exc_info=True)
132
+ pass
133
+
134
+
135
+ def _cleanup_span(span: Span, wrapper: TracerWrapper):
136
+ """Clean up span and context."""
137
+ span.end()
138
+ wrapper.pop_span_context()
139
+
140
+
141
+ def observe_base(
142
+ *,
143
+ name: str | None = None,
144
+ ignore_input: bool = False,
145
+ ignore_inputs: list[str] | None = None,
146
+ ignore_output: bool = False,
147
+ span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
148
+ metadata: dict[str, Any] | None = None,
149
+ association_properties: dict[str, Any] | None = None,
150
+ input_formatter: Callable[..., str] | None = None,
151
+ output_formatter: Callable[..., str] | None = None,
152
+ preserve_global_context: bool = False,
153
+ ) -> Callable[[F], F]:
154
+ def decorate(fn: F) -> F:
155
+ @wraps(fn)
156
+ def wrap(*args, **kwargs):
157
+ if not TracerWrapper.verify_initialized():
158
+ return fn(*args, **kwargs)
159
+
160
+ span_name = name or fn.__name__
161
+ wrapper = TracerWrapper()
162
+
163
+ span = _setup_span(
164
+ span_name,
165
+ span_type,
166
+ association_properties,
167
+ preserve_global_context,
168
+ metadata,
169
+ )
170
+
171
+ # Set association props in context before push_span_context
172
+ # so child spans inherit them
173
+ assoc_props_token = set_association_props_in_context(span)
174
+ if assoc_props_token and isinstance(span, LaminarSpan):
175
+ span._lmnr_assoc_props_token = assoc_props_token
176
+
177
+ new_context = wrapper.push_span_context(span)
178
+ # Some auto-instrumentations are not under our control, so they
179
+ # don't have access to our isolated context. We attach the context
180
+ # to the OTEL global context, so that spans know their parent
181
+ # span and trace_id.
182
+ ctx_token = context_api.attach(new_context)
183
+ # update our isolated context too
184
+ isolated_ctx_token = attach_context(new_context)
185
+
186
+ _process_input(
187
+ span, fn, args, kwargs, ignore_input, ignore_inputs, input_formatter
188
+ )
189
+
190
+ try:
191
+ res = fn(*args, **kwargs)
192
+ except Exception as e:
193
+ _process_exception(span, e)
194
+ _cleanup_span(span, wrapper)
195
+ raise
196
+ finally:
197
+ # Always restore global context
198
+ context_api.detach(ctx_token)
199
+ detach_context(isolated_ctx_token)
200
+ # span will be ended in the generator
201
+ if isinstance(res, types.GeneratorType):
202
+ return _handle_generator(span, wrapper, res)
203
+ if isinstance(res, types.AsyncGeneratorType):
204
+ # async def foo() -> AsyncGenerator[int, None]:
205
+ # is not considered async in a classical sense in Python,
206
+ # so we handle this inside the sync wrapper.
207
+ # In particular, CO_COROUTINE is different from CO_ASYNC_GENERATOR.
208
+ # Flags are listed from LSB here:
209
+ # https://docs.python.org/3/library/inspect.html#inspect-module-co-flags
210
+ # See also: https://groups.google.com/g/python-tulip/c/6rWweGXLutU?pli=1
211
+ return _ahandle_generator(span, wrapper, res)
212
+
213
+ _process_output(span, res, ignore_output, output_formatter)
214
+ _cleanup_span(span, wrapper)
215
+ return res
216
+
217
+ return wrap
218
+
219
+ return decorate
220
+
221
+
222
+ # Async Decorators
223
+ def async_observe_base(
224
+ *,
225
+ name: str | None = None,
226
+ ignore_input: bool = False,
227
+ ignore_inputs: list[str] | None = None,
228
+ ignore_output: bool = False,
229
+ span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
230
+ metadata: dict[str, Any] | None = None,
231
+ association_properties: dict[str, Any] | None = None,
232
+ input_formatter: Callable[..., str] | None = None,
233
+ output_formatter: Callable[..., str] | None = None,
234
+ preserve_global_context: bool = False,
235
+ ) -> Callable[[F], F]:
236
+ def decorate(fn: F) -> F:
237
+ @wraps(fn)
238
+ async def wrap(*args, **kwargs):
239
+ if not TracerWrapper.verify_initialized():
240
+ return await fn(*args, **kwargs)
241
+
242
+ span_name = name or fn.__name__
243
+ wrapper = TracerWrapper()
244
+
245
+ span = _setup_span(
246
+ span_name,
247
+ span_type,
248
+ association_properties,
249
+ preserve_global_context,
250
+ metadata,
251
+ )
252
+
253
+ # Set association props in context before push_span_context
254
+ # so child spans inherit them
255
+ assoc_props_token = set_association_props_in_context(span)
256
+ if assoc_props_token and isinstance(span, LaminarSpan):
257
+ span._lmnr_assoc_props_token = assoc_props_token
258
+
259
+ new_context = wrapper.push_span_context(span)
260
+ # Some auto-instrumentations are not under our control, so they
261
+ # don't have access to our isolated context. We attach the context
262
+ # to the OTEL global context, so that spans know their parent
263
+ # span and trace_id.
264
+ ctx_token = context_api.attach(new_context)
265
+ # update our isolated context too
266
+ isolated_ctx_token = attach_context(new_context)
267
+
268
+ _process_input(
269
+ span, fn, args, kwargs, ignore_input, ignore_inputs, input_formatter
270
+ )
271
+
272
+ try:
273
+ res = await fn(*args, **kwargs)
274
+ except Exception as e:
275
+ _process_exception(span, e)
276
+ _cleanup_span(span, wrapper)
277
+ raise e
278
+ finally:
279
+ # Always restore global context
280
+ context_api.detach(ctx_token)
281
+ detach_context(isolated_ctx_token)
282
+
283
+ # span will be ended in the generator
284
+ if isinstance(res, types.AsyncGeneratorType):
285
+ # probably unreachable, read the comment in the similar
286
+ # part of the sync wrapper.
287
+ return await _ahandle_generator(span, wrapper, res)
288
+
289
+ _process_output(span, res, ignore_output, output_formatter)
290
+ _cleanup_span(span, wrapper)
291
+ return res
292
+
293
+ return wrap
294
+
295
+ return decorate
296
+
297
+
298
+ def _handle_generator(
299
+ span: Span,
300
+ wrapper: TracerWrapper,
301
+ res: Generator,
302
+ ignore_output: bool = False,
303
+ output_formatter: Callable[..., str] | None = None,
304
+ ):
305
+ results = []
306
+ try:
307
+ for part in res:
308
+ results.append(part)
309
+ yield part
310
+ finally:
311
+ _process_output(span, results, ignore_output, output_formatter)
312
+ _cleanup_span(span, wrapper)
313
+
314
+
315
+ async def _ahandle_generator(
316
+ span: Span,
317
+ wrapper: TracerWrapper,
318
+ res: AsyncGenerator,
319
+ ignore_output: bool = False,
320
+ output_formatter: Callable[..., str] | None = None,
321
+ ):
322
+ results = []
323
+ try:
324
+ async for part in res:
325
+ results.append(part)
326
+ yield part
327
+ finally:
328
+ _process_output(span, results, ignore_output, output_formatter)
329
+ _cleanup_span(span, wrapper)
330
+
331
+
332
+ def _process_exception(span: Span, e: Exception):
333
+ # Note that this `escaped` is sent as a StringValue("True"), not a boolean.
334
+ span.record_exception(
335
+ e, attributes=get_event_attributes_from_context(), escaped=True
336
+ )
337
+ span.set_status(Status(StatusCode.ERROR, str(e)))