lmnr 0.5.3__py3-none-any.whl → 0.6.0__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 (32) hide show
  1. lmnr/__init__.py +6 -1
  2. lmnr/opentelemetry_lib/__init__.py +16 -36
  3. lmnr/opentelemetry_lib/decorators/__init__.py +219 -0
  4. lmnr/opentelemetry_lib/tracing/__init__.py +139 -1
  5. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +398 -0
  6. lmnr/opentelemetry_lib/tracing/attributes.py +14 -7
  7. lmnr/opentelemetry_lib/tracing/context_properties.py +53 -0
  8. lmnr/opentelemetry_lib/tracing/exporter.py +60 -0
  9. lmnr/opentelemetry_lib/tracing/instruments.py +121 -0
  10. lmnr/opentelemetry_lib/tracing/processor.py +96 -0
  11. lmnr/opentelemetry_lib/tracing/{context_manager.py → tracer.py} +6 -1
  12. lmnr/opentelemetry_lib/utils/package_check.py +3 -1
  13. lmnr/sdk/browser/browser_use_otel.py +1 -1
  14. lmnr/sdk/browser/pw_utils.py +8 -4
  15. lmnr/sdk/client/asynchronous/resources/agent.py +3 -1
  16. lmnr/sdk/client/synchronous/resources/agent.py +3 -1
  17. lmnr/sdk/decorators.py +4 -2
  18. lmnr/sdk/evaluations.py +3 -3
  19. lmnr/sdk/laminar.py +13 -31
  20. lmnr/sdk/utils.py +2 -3
  21. lmnr/version.py +1 -1
  22. {lmnr-0.5.3.dist-info → lmnr-0.6.0.dist-info}/METADATA +64 -62
  23. {lmnr-0.5.3.dist-info → lmnr-0.6.0.dist-info}/RECORD +26 -27
  24. lmnr/opentelemetry_lib/config/__init__.py +0 -12
  25. lmnr/opentelemetry_lib/decorators/base.py +0 -210
  26. lmnr/opentelemetry_lib/instruments.py +0 -42
  27. lmnr/opentelemetry_lib/tracing/content_allow_list.py +0 -24
  28. lmnr/opentelemetry_lib/tracing/tracing.py +0 -1016
  29. lmnr/opentelemetry_lib/utils/in_memory_span_exporter.py +0 -61
  30. {lmnr-0.5.3.dist-info → lmnr-0.6.0.dist-info}/LICENSE +0 -0
  31. {lmnr-0.5.3.dist-info → lmnr-0.6.0.dist-info}/WHEEL +0 -0
  32. {lmnr-0.5.3.dist-info → lmnr-0.6.0.dist-info}/entry_points.txt +0 -0
lmnr/__init__.py CHANGED
@@ -13,8 +13,10 @@ from .sdk.types import (
13
13
  )
14
14
  from .sdk.decorators import observe
15
15
  from .sdk.types import LaminarSpanContext
16
- from .opentelemetry_lib import Instruments
17
16
  from .opentelemetry_lib.tracing.attributes import Attributes
17
+ from .opentelemetry_lib.tracing.instruments import Instruments
18
+ from .opentelemetry_lib.tracing.processor import LaminarSpanProcessor
19
+ from .opentelemetry_lib.tracing.tracer import get_laminar_tracer_provider, get_tracer
18
20
  from opentelemetry.trace import use_span
19
21
 
20
22
  __all__ = [
@@ -29,9 +31,12 @@ __all__ = [
29
31
  "LaminarClient",
30
32
  "LaminarDataset",
31
33
  "LaminarSpanContext",
34
+ "LaminarSpanProcessor",
32
35
  "RunAgentResponseChunk",
33
36
  "StepChunkContent",
34
37
  "TracingLevel",
38
+ "get_laminar_tracer_provider",
39
+ "get_tracer",
35
40
  "evaluate",
36
41
  "observe",
37
42
  "use_span",
@@ -1,19 +1,13 @@
1
1
  import sys
2
2
 
3
3
  from typing import Optional, Set
4
- from opentelemetry.sdk.trace import SpanProcessor
5
4
  from opentelemetry.sdk.trace.export import SpanExporter
6
5
  from opentelemetry.sdk.resources import SERVICE_NAME
7
- from opentelemetry.propagators.textmap import TextMapPropagator
8
- from opentelemetry.util.re import parse_env_headers
9
6
 
10
- from lmnr.opentelemetry_lib.instruments import Instruments
11
- from lmnr.opentelemetry_lib.config import (
12
- is_content_tracing_enabled,
13
- is_tracing_enabled,
14
- )
15
- from lmnr.opentelemetry_lib.tracing.tracing import TracerWrapper
16
- from typing import Dict
7
+ from lmnr.opentelemetry_lib.tracing.instruments import Instruments
8
+ from lmnr.opentelemetry_lib.tracing import TracerWrapper
9
+
10
+ MAX_MANUAL_SPAN_PAYLOAD_SIZE = 1024 * 1024 # 1MB
17
11
 
18
12
 
19
13
  class TracerManager:
@@ -22,48 +16,34 @@ class TracerManager:
22
16
  @staticmethod
23
17
  def init(
24
18
  app_name: Optional[str] = sys.argv[0],
25
- api_endpoint: str = "https://api.lmnr.ai",
26
- api_key: Optional[str] = None,
27
- headers: Dict[str, str] = {},
28
19
  disable_batch=False,
29
20
  exporter: Optional[SpanExporter] = None,
30
- processor: Optional[SpanProcessor] = None,
31
- propagator: Optional[TextMapPropagator] = None,
32
- should_enrich_metrics: bool = False,
33
21
  resource_attributes: dict = {},
34
22
  instruments: Optional[Set[Instruments]] = None,
35
- base_http_url: Optional[str] = None,
23
+ base_url: str = "https://api.lmnr.ai",
24
+ port: int = 8443,
25
+ http_port: int = 443,
36
26
  project_api_key: Optional[str] = None,
37
27
  max_export_batch_size: Optional[int] = None,
28
+ force_http: bool = False,
29
+ timeout_seconds: int = 30,
38
30
  ) -> None:
39
- if not is_tracing_enabled():
40
- return
41
-
42
- enable_content_tracing = is_content_tracing_enabled()
43
-
44
- if isinstance(headers, str):
45
- headers = parse_env_headers(headers)
46
-
47
- if api_key and not exporter and not processor and not headers:
48
- headers = {
49
- "Authorization": f"Bearer {api_key}",
50
- }
31
+ enable_content_tracing = True
51
32
 
52
33
  # Tracer init
53
34
  resource_attributes.update({SERVICE_NAME: app_name})
54
- TracerWrapper.set_static_params(
55
- resource_attributes, enable_content_tracing, api_endpoint, headers
56
- )
35
+ TracerWrapper.set_static_params(resource_attributes, enable_content_tracing)
57
36
  TracerManager.__tracer_wrapper = TracerWrapper(
58
37
  disable_batch=disable_batch,
59
- processor=processor,
60
- propagator=propagator,
61
38
  exporter=exporter,
62
- should_enrich_metrics=should_enrich_metrics,
63
39
  instruments=instruments,
64
- base_http_url=base_http_url,
40
+ base_url=base_url,
41
+ port=port,
42
+ http_port=http_port,
65
43
  project_api_key=project_api_key,
66
44
  max_export_batch_size=max_export_batch_size,
45
+ force_http=force_http,
46
+ timeout_seconds=timeout_seconds,
67
47
  )
68
48
 
69
49
  @staticmethod
@@ -0,0 +1,219 @@
1
+ from functools import wraps
2
+ import json
3
+ import logging
4
+ import pydantic
5
+ import types
6
+ from typing import Any, Literal, Optional, Union
7
+
8
+ from opentelemetry import trace
9
+ from opentelemetry import context as context_api
10
+ from opentelemetry.trace import Span
11
+
12
+ from lmnr.sdk.utils import get_input_from_func_args, is_method
13
+ from lmnr.opentelemetry_lib import MAX_MANUAL_SPAN_PAYLOAD_SIZE
14
+ from lmnr.opentelemetry_lib.tracing.tracer import get_tracer
15
+ from lmnr.opentelemetry_lib.tracing.attributes import SPAN_INPUT, SPAN_OUTPUT, SPAN_TYPE
16
+ from lmnr.opentelemetry_lib.tracing import TracerWrapper
17
+ from lmnr.opentelemetry_lib.utils.json_encoder import JSONEncoder
18
+
19
+
20
+ class CustomJSONEncoder(JSONEncoder):
21
+ def default(self, o: Any) -> Any:
22
+ if isinstance(o, pydantic.BaseModel):
23
+ return o.model_dump_json()
24
+ try:
25
+ return super().default(o)
26
+ except TypeError:
27
+ return str(o) # Fallback to string representation for unsupported types
28
+
29
+
30
+ def json_dumps(data: dict) -> str:
31
+ try:
32
+ return json.dumps(data, cls=CustomJSONEncoder)
33
+ except Exception:
34
+ # Log the exception and return a placeholder if serialization completely fails
35
+ logging.warning("Failed to serialize data to JSON, type: %s", type(data))
36
+ return "{}" # Return an empty JSON object as a fallback
37
+
38
+
39
+ def entity_method(
40
+ name: Optional[str] = None,
41
+ ignore_input: bool = False,
42
+ ignore_inputs: Optional[list[str]] = None,
43
+ ignore_output: bool = False,
44
+ span_type: Union[Literal["DEFAULT"], Literal["LLM"], Literal["TOOL"]] = "DEFAULT",
45
+ ):
46
+ def decorate(fn):
47
+ @wraps(fn)
48
+ def wrap(*args, **kwargs):
49
+ if not TracerWrapper.verify_initialized():
50
+ return fn(*args, **kwargs)
51
+
52
+ span_name = name or fn.__name__
53
+
54
+ with get_tracer() as tracer:
55
+ span = tracer.start_span(span_name, attributes={SPAN_TYPE: span_type})
56
+
57
+ ctx = trace.set_span_in_context(span, context_api.get_current())
58
+ ctx_token = context_api.attach(ctx)
59
+
60
+ try:
61
+ if not ignore_input:
62
+ inp = json_dumps(
63
+ get_input_from_func_args(
64
+ fn,
65
+ is_method=is_method(fn),
66
+ func_args=args,
67
+ func_kwargs=kwargs,
68
+ ignore_inputs=ignore_inputs,
69
+ )
70
+ )
71
+ if len(inp) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
72
+ span.set_attribute(
73
+ SPAN_INPUT, "Laminar: input too large to record"
74
+ )
75
+ else:
76
+ span.set_attribute(SPAN_INPUT, inp)
77
+ except TypeError:
78
+ pass
79
+
80
+ try:
81
+ res = fn(*args, **kwargs)
82
+ except Exception as e:
83
+ _process_exception(span, e)
84
+ span.end()
85
+ raise e
86
+
87
+ # span will be ended in the generator
88
+ if isinstance(res, types.GeneratorType):
89
+ return _handle_generator(span, ctx_token, res)
90
+ if isinstance(res, types.AsyncGeneratorType):
91
+ # async def foo() -> AsyncGenerator[int, None]:
92
+ # is not considered async in a classical sense in Python,
93
+ # so we handle this inside the sync wrapper.
94
+ # In particular, CO_COROUTINE is different from CO_ASYNC_GENERATOR.
95
+ # Flags are listed from LSB here:
96
+ # https://docs.python.org/3/library/inspect.html#inspect-module-co-flags
97
+ # See also: https://groups.google.com/g/python-tulip/c/6rWweGXLutU?pli=1
98
+ return _ahandle_generator(span, ctx_token, res)
99
+
100
+ try:
101
+ if not ignore_output:
102
+ output = json_dumps(res)
103
+ if len(output) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
104
+ span.set_attribute(
105
+ SPAN_OUTPUT, "Laminar: output too large to record"
106
+ )
107
+ else:
108
+ span.set_attribute(SPAN_OUTPUT, output)
109
+ except TypeError:
110
+ pass
111
+
112
+ span.end()
113
+ context_api.detach(ctx_token)
114
+
115
+ return res
116
+
117
+ return wrap
118
+
119
+ return decorate
120
+
121
+
122
+ # Async Decorators
123
+ def aentity_method(
124
+ name: Optional[str] = None,
125
+ ignore_input: bool = False,
126
+ ignore_inputs: Optional[list[str]] = None,
127
+ ignore_output: bool = False,
128
+ span_type: Union[Literal["DEFAULT"], Literal["LLM"], Literal["TOOL"]] = "DEFAULT",
129
+ ):
130
+ def decorate(fn):
131
+ @wraps(fn)
132
+ async def wrap(*args, **kwargs):
133
+ if not TracerWrapper.verify_initialized():
134
+ return await fn(*args, **kwargs)
135
+
136
+ span_name = name or fn.__name__
137
+
138
+ with get_tracer() as tracer:
139
+ span = tracer.start_span(span_name, attributes={SPAN_TYPE: span_type})
140
+
141
+ ctx = trace.set_span_in_context(span, context_api.get_current())
142
+ ctx_token = context_api.attach(ctx)
143
+
144
+ try:
145
+ if not ignore_input:
146
+ inp = json_dumps(
147
+ get_input_from_func_args(
148
+ fn,
149
+ is_method=is_method(fn),
150
+ func_args=args,
151
+ func_kwargs=kwargs,
152
+ ignore_inputs=ignore_inputs,
153
+ )
154
+ )
155
+ if len(inp) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
156
+ span.set_attribute(
157
+ SPAN_INPUT, "Laminar: input too large to record"
158
+ )
159
+ else:
160
+ span.set_attribute(SPAN_INPUT, inp)
161
+ except TypeError:
162
+ pass
163
+
164
+ try:
165
+ res = await fn(*args, **kwargs)
166
+ except Exception as e:
167
+ _process_exception(span, e)
168
+ span.end()
169
+ raise e
170
+
171
+ # span will be ended in the generator
172
+ if isinstance(res, types.AsyncGeneratorType):
173
+ # probably unreachable, read the comment in the similar
174
+ # part of the sync wrapper.
175
+ return await _ahandle_generator(span, ctx_token, res)
176
+
177
+ try:
178
+ if not ignore_output:
179
+ output = json_dumps(res)
180
+ if len(output) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
181
+ span.set_attribute(
182
+ SPAN_OUTPUT, "Laminar: output too large to record"
183
+ )
184
+ else:
185
+ span.set_attribute(SPAN_OUTPUT, output)
186
+ except TypeError:
187
+ pass
188
+
189
+ span.end()
190
+ context_api.detach(ctx_token)
191
+
192
+ return res
193
+
194
+ return wrap
195
+
196
+ return decorate
197
+
198
+
199
+ def _handle_generator(span, ctx_token, res):
200
+ yield from res
201
+
202
+ span.end()
203
+ if ctx_token is not None:
204
+ context_api.detach(ctx_token)
205
+
206
+
207
+ async def _ahandle_generator(span, ctx_token, res):
208
+ # async with contextlib.aclosing(res) as closing_gen:
209
+ async for part in res:
210
+ yield part
211
+
212
+ span.end()
213
+ if ctx_token is not None:
214
+ context_api.detach(ctx_token)
215
+
216
+
217
+ def _process_exception(span: Span, e: Exception):
218
+ # Note that this `escaped` is sent as a StringValue("True"), not a boolean.
219
+ span.record_exception(e, escaped=True)
@@ -1 +1,139 @@
1
- from lmnr.opentelemetry_lib.tracing.context_manager import get_tracer
1
+ import atexit
2
+ import logging
3
+
4
+ from lmnr.opentelemetry_lib.tracing.processor import LaminarSpanProcessor
5
+ from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
6
+ from lmnr.sdk.client.synchronous.sync_client import LaminarClient
7
+ from lmnr.sdk.log import VerboseColorfulFormatter
8
+ from lmnr.opentelemetry_lib.tracing.instruments import (
9
+ Instruments,
10
+ init_instrumentations,
11
+ )
12
+
13
+ from opentelemetry import trace
14
+ from opentelemetry.sdk.resources import Resource
15
+ from opentelemetry.sdk.trace import TracerProvider, SpanProcessor
16
+ from opentelemetry.sdk.trace.export import SpanExporter
17
+
18
+ from typing import Optional, Set
19
+
20
+
21
+ module_logger = logging.getLogger(__name__)
22
+ console_log_handler = logging.StreamHandler()
23
+ console_log_handler.setFormatter(VerboseColorfulFormatter())
24
+ module_logger.addHandler(console_log_handler)
25
+
26
+
27
+ TRACER_NAME = "lmnr.tracer"
28
+
29
+ MAX_EVENTS_OR_ATTRIBUTES_PER_SPAN = 5000
30
+
31
+
32
+ class TracerWrapper(object):
33
+ resource_attributes: dict = {}
34
+ enable_content_tracing: bool = True
35
+ __tracer_provider: Optional[TracerProvider] = None
36
+ __logger: logging.Logger
37
+ __client: LaminarClient
38
+ __async_client: AsyncLaminarClient
39
+ __resource: Resource
40
+ __span_processor: SpanProcessor
41
+
42
+ def __new__(
43
+ cls,
44
+ disable_batch=False,
45
+ exporter: Optional[SpanExporter] = None,
46
+ instruments: Optional[Set[Instruments]] = None,
47
+ block_instruments: Optional[Set[Instruments]] = None,
48
+ base_url: str = "https://api.lmnr.ai",
49
+ port: int = 8443,
50
+ http_port: int = 443,
51
+ project_api_key: Optional[str] = None,
52
+ max_export_batch_size: Optional[int] = None,
53
+ force_http: bool = False,
54
+ timeout_seconds: int = 10,
55
+ ) -> "TracerWrapper":
56
+ base_http_url = f"{base_url}:{http_port}"
57
+ cls._initialize_logger(cls)
58
+ if not hasattr(cls, "instance"):
59
+ obj = cls.instance = super(TracerWrapper, cls).__new__(cls)
60
+
61
+ obj.__client = LaminarClient(
62
+ base_url=base_http_url,
63
+ project_api_key=project_api_key,
64
+ )
65
+ obj.__async_client = AsyncLaminarClient(
66
+ base_url=base_http_url,
67
+ project_api_key=project_api_key,
68
+ )
69
+
70
+ obj.__resource = Resource(attributes=TracerWrapper.resource_attributes)
71
+ obj.__tracer_provider = TracerProvider(resource=obj.__resource)
72
+
73
+ obj.__span_processor = LaminarSpanProcessor(
74
+ base_url=base_url,
75
+ api_key=project_api_key,
76
+ port=http_port if force_http else port,
77
+ exporter=exporter,
78
+ max_export_batch_size=max_export_batch_size,
79
+ timeout_seconds=timeout_seconds,
80
+ force_http=force_http,
81
+ disable_batch=disable_batch,
82
+ )
83
+
84
+ obj.__tracer_provider.add_span_processor(obj.__span_processor)
85
+
86
+ init_instrumentations(
87
+ tracer_provider=obj.__tracer_provider,
88
+ instruments=instruments,
89
+ block_instruments=block_instruments,
90
+ client=obj.__client,
91
+ async_client=obj.__async_client,
92
+ )
93
+
94
+ # Force flushes for debug environments (e.g. local development)
95
+ atexit.register(obj.exit_handler)
96
+
97
+ return cls.instance
98
+
99
+ def exit_handler(self):
100
+ if isinstance(self.__span_processor, LaminarSpanProcessor):
101
+ self.__span_processor.clear()
102
+ self.flush()
103
+
104
+ def _initialize_logger(self):
105
+ self.__logger = logging.getLogger(__name__)
106
+ console_log_handler = logging.StreamHandler()
107
+ console_log_handler.setFormatter(VerboseColorfulFormatter())
108
+ self.__logger.addHandler(console_log_handler)
109
+
110
+ @staticmethod
111
+ def set_static_params(
112
+ resource_attributes: dict,
113
+ enable_content_tracing: bool,
114
+ ) -> None:
115
+ TracerWrapper.resource_attributes = resource_attributes
116
+ TracerWrapper.enable_content_tracing = enable_content_tracing
117
+
118
+ @classmethod
119
+ def verify_initialized(cls) -> bool:
120
+ return hasattr(cls, "instance")
121
+
122
+ @classmethod
123
+ def clear(cls):
124
+ # Any state cleanup. Now used in between tests
125
+ if isinstance(cls.instance.__span_processor, LaminarSpanProcessor):
126
+ cls.instance.__span_processor.clear()
127
+
128
+ def shutdown(self):
129
+ if self.__tracer_provider is None:
130
+ return
131
+ self.__tracer_provider.shutdown()
132
+
133
+ def flush(self):
134
+ return self.__span_processor.force_flush()
135
+
136
+ def get_tracer(self):
137
+ if self.__tracer_provider is None:
138
+ return trace.get_tracer_provider().get_tracer(TRACER_NAME)
139
+ return self.__tracer_provider.get_tracer(TRACER_NAME)