lmnr 0.5.2__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 (48) hide show
  1. lmnr/__init__.py +7 -2
  2. lmnr/cli.py +10 -8
  3. lmnr/opentelemetry_lib/__init__.py +55 -0
  4. lmnr/{openllmetry_sdk/decorators/base.py → opentelemetry_lib/decorators/__init__.py} +24 -15
  5. lmnr/{openllmetry_sdk → opentelemetry_lib}/opentelemetry/instrumentation/google_genai/utils.py +1 -1
  6. lmnr/opentelemetry_lib/tracing/__init__.py +139 -0
  7. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +398 -0
  8. lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +14 -7
  9. lmnr/opentelemetry_lib/tracing/context_properties.py +53 -0
  10. lmnr/opentelemetry_lib/tracing/exporter.py +60 -0
  11. lmnr/opentelemetry_lib/tracing/instruments.py +121 -0
  12. lmnr/opentelemetry_lib/tracing/processor.py +96 -0
  13. lmnr/{openllmetry_sdk/tracing/context_manager.py → opentelemetry_lib/tracing/tracer.py} +6 -1
  14. lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/package_check.py +3 -1
  15. lmnr/sdk/browser/browser_use_otel.py +20 -3
  16. lmnr/sdk/browser/patchright_otel.py +177 -0
  17. lmnr/sdk/browser/playwright_otel.py +16 -7
  18. lmnr/sdk/browser/pw_utils.py +116 -74
  19. lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +98 -0
  20. lmnr/sdk/client/asynchronous/resources/agent.py +22 -1
  21. lmnr/sdk/client/synchronous/resources/agent.py +23 -1
  22. lmnr/sdk/decorators.py +5 -3
  23. lmnr/sdk/eval_control.py +3 -2
  24. lmnr/sdk/evaluations.py +10 -16
  25. lmnr/sdk/laminar.py +16 -34
  26. lmnr/sdk/types.py +2 -0
  27. lmnr/sdk/utils.py +2 -3
  28. lmnr/version.py +1 -1
  29. {lmnr-0.5.2.dist-info → lmnr-0.6.0.dist-info}/METADATA +65 -63
  30. lmnr-0.6.0.dist-info/RECORD +54 -0
  31. {lmnr-0.5.2.dist-info → lmnr-0.6.0.dist-info}/WHEEL +1 -1
  32. lmnr/openllmetry_sdk/__init__.py +0 -75
  33. lmnr/openllmetry_sdk/config/__init__.py +0 -12
  34. lmnr/openllmetry_sdk/decorators/__init__.py +0 -0
  35. lmnr/openllmetry_sdk/instruments.py +0 -41
  36. lmnr/openllmetry_sdk/tracing/__init__.py +0 -1
  37. lmnr/openllmetry_sdk/tracing/content_allow_list.py +0 -24
  38. lmnr/openllmetry_sdk/tracing/tracing.py +0 -998
  39. lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py +0 -61
  40. lmnr/sdk/browser/rrweb/rrweb.min.js +0 -18
  41. lmnr-0.5.2.dist-info/RECORD +0 -54
  42. /lmnr/{openllmetry_sdk → opentelemetry_lib}/.flake8 +0 -0
  43. /lmnr/{openllmetry_sdk → opentelemetry_lib}/opentelemetry/instrumentation/google_genai/__init__.py +0 -0
  44. /lmnr/{openllmetry_sdk → opentelemetry_lib}/opentelemetry/instrumentation/google_genai/config.py +0 -0
  45. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/__init__.py +0 -0
  46. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
  47. {lmnr-0.5.2.dist-info → lmnr-0.6.0.dist-info}/LICENSE +0 -0
  48. {lmnr-0.5.2.dist-info → lmnr-0.6.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,96 @@
1
+ import uuid
2
+
3
+ from typing import Optional, Union
4
+ from opentelemetry.sdk.trace.export import (
5
+ SpanProcessor,
6
+ SpanExporter,
7
+ BatchSpanProcessor,
8
+ SimpleSpanProcessor,
9
+ )
10
+ from opentelemetry.trace import Span
11
+ from opentelemetry.context import Context, get_value, get_current, set_value
12
+
13
+ from lmnr.opentelemetry_lib.tracing.attributes import (
14
+ SPAN_IDS_PATH,
15
+ SPAN_INSTRUMENTATION_SOURCE,
16
+ SPAN_LANGUAGE_VERSION,
17
+ SPAN_PATH,
18
+ SPAN_SDK_VERSION,
19
+ )
20
+ from lmnr.opentelemetry_lib.tracing.exporter import LaminarSpanExporter
21
+ from lmnr.opentelemetry_lib.tracing.context_properties import (
22
+ _set_association_properties_attributes,
23
+ )
24
+ from lmnr.version import PYTHON_VERSION, __version__
25
+
26
+
27
+ class LaminarSpanProcessor(SpanProcessor):
28
+ instance: Union[BatchSpanProcessor, SimpleSpanProcessor]
29
+ __span_id_to_path: dict[int, list[str]] = {}
30
+ __span_id_lists: dict[int, list[str]] = {}
31
+
32
+ def __init__(
33
+ self,
34
+ base_url: Optional[str] = None,
35
+ port: Optional[int] = None,
36
+ api_key: Optional[str] = None,
37
+ timeout_seconds: int = 30,
38
+ force_http: bool = False,
39
+ max_export_batch_size: int = 512,
40
+ disable_batch: bool = False,
41
+ exporter: Optional[SpanExporter] = None,
42
+ ):
43
+ self.exporter = exporter or LaminarSpanExporter(
44
+ base_url=base_url,
45
+ port=port,
46
+ api_key=api_key,
47
+ timeout_seconds=timeout_seconds,
48
+ force_http=force_http,
49
+ )
50
+ self.instance = (
51
+ SimpleSpanProcessor(self.exporter)
52
+ if disable_batch
53
+ else BatchSpanProcessor(
54
+ self.exporter, max_export_batch_size=max_export_batch_size
55
+ )
56
+ )
57
+
58
+ def on_start(self, span: Span, parent_context: Optional[Context] = None):
59
+ span_path_in_context = get_value("span_path", parent_context or get_current())
60
+ parent_span_path = span_path_in_context or (
61
+ self.__span_id_to_path.get(span.parent.span_id) if span.parent else None
62
+ )
63
+ parent_span_ids_path = (
64
+ self.__span_id_lists.get(span.parent.span_id, []) if span.parent else []
65
+ )
66
+ span_path = parent_span_path + [span.name] if parent_span_path else [span.name]
67
+ span_ids_path = parent_span_ids_path + [
68
+ str(uuid.UUID(int=span.get_span_context().span_id))
69
+ ]
70
+ span.set_attribute(SPAN_PATH, span_path)
71
+ span.set_attribute(SPAN_IDS_PATH, span_ids_path)
72
+ set_value("span_path", span_path, get_current())
73
+ self.__span_id_to_path[span.get_span_context().span_id] = span_path
74
+ self.__span_id_lists[span.get_span_context().span_id] = span_ids_path
75
+
76
+ span.set_attribute(SPAN_INSTRUMENTATION_SOURCE, "python")
77
+ span.set_attribute(SPAN_SDK_VERSION, __version__)
78
+ span.set_attribute(SPAN_LANGUAGE_VERSION, f"python@{PYTHON_VERSION}")
79
+
80
+ association_properties = get_value("association_properties")
81
+ if association_properties is not None:
82
+ _set_association_properties_attributes(span, association_properties)
83
+ self.instance.on_start(span, parent_context)
84
+
85
+ def on_end(self, span: Span):
86
+ self.instance.on_end(span)
87
+
88
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
89
+ return self.instance.force_flush(timeout_millis)
90
+
91
+ def shutdown(self):
92
+ self.instance.shutdown()
93
+
94
+ def clear(self):
95
+ self.__span_id_to_path = {}
96
+ self.__span_id_lists = {}
@@ -1,6 +1,11 @@
1
1
  from contextlib import contextmanager
2
2
 
3
- from lmnr.openllmetry_sdk.tracing.tracing import TracerWrapper
3
+ from opentelemetry import trace
4
+ from lmnr.opentelemetry_lib.tracing import TracerWrapper
5
+
6
+
7
+ def get_laminar_tracer_provider() -> trace.TracerProvider:
8
+ return TracerWrapper.instance.__tracer_provider or trace.get_tracer_provider()
4
9
 
5
10
 
6
11
  @contextmanager
@@ -1,6 +1,8 @@
1
1
  from importlib.metadata import distributions
2
2
 
3
- installed_packages = {dist.metadata.get("Name", "").lower() for dist in distributions()}
3
+ installed_packages = {
4
+ (dist.name or dist.metadata.get("Name", "")).lower() for dist in distributions()
5
+ }
4
6
 
5
7
 
6
8
  def is_package_installed(package_name: str) -> bool:
@@ -1,4 +1,4 @@
1
- from lmnr.openllmetry_sdk.decorators.base import json_dumps
1
+ from lmnr.opentelemetry_lib.decorators import json_dumps
2
2
  from lmnr.sdk.browser.utils import with_tracer_wrapper
3
3
  from lmnr.sdk.utils import get_input_from_func_args
4
4
  from lmnr.version import __version__
@@ -8,6 +8,16 @@ from opentelemetry.instrumentation.utils import unwrap
8
8
  from opentelemetry.trace import get_tracer, Tracer
9
9
  from typing import Collection
10
10
  from wrapt import wrap_function_wrapper
11
+ import pydantic
12
+
13
+ try:
14
+ from browser_use import AgentHistoryList
15
+ except ImportError as e:
16
+ raise ImportError(
17
+ f"Attempted to import {__file__}, but it is designed "
18
+ "to patch Browser Use, which is not installed. Use `pip install browser-use` "
19
+ "to install Browser Use or remove this import."
20
+ ) from e
11
21
 
12
22
  _instruments = ("browser-use >= 0.1.0",)
13
23
 
@@ -18,7 +28,7 @@ WRAPPED_METHODS = [
18
28
  "method": "run",
19
29
  "span_name": "agent.run",
20
30
  "ignore_input": False,
21
- "ignore_output": True,
31
+ "ignore_output": False,
22
32
  "span_type": "DEFAULT",
23
33
  },
24
34
  {
@@ -73,7 +83,14 @@ async def _wrap(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
73
83
  span.set_attributes(attributes)
74
84
  result = await wrapped(*args, **kwargs)
75
85
  if not to_wrap.get("ignore_output"):
76
- span.set_attribute("lmnr.span.output", json_dumps(result))
86
+ if isinstance(result, AgentHistoryList):
87
+ result = result.final_result()
88
+ serialized = (
89
+ result.model_dump_json()
90
+ if isinstance(result, pydantic.BaseModel)
91
+ else json_dumps(result)
92
+ )
93
+ span.set_attribute("lmnr.span.output", serialized)
77
94
  return result
78
95
 
79
96
 
@@ -0,0 +1,177 @@
1
+ from lmnr.sdk.browser.playwright_otel import (
2
+ _wrap_new_page,
3
+ _wrap_new_page_async,
4
+ _wrap_new_browser_sync,
5
+ _wrap_new_browser_async,
6
+ _wrap_new_context_sync,
7
+ _wrap_new_context_async,
8
+ _wrap_close_browser_sync,
9
+ _wrap_close_browser_async,
10
+ )
11
+ from lmnr.sdk.client.synchronous.sync_client import LaminarClient
12
+ from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
13
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
14
+ from opentelemetry.instrumentation.utils import unwrap
15
+ from opentelemetry.trace import get_tracer
16
+ from lmnr.version import __version__
17
+ from typing import Collection
18
+ from wrapt import wrap_function_wrapper
19
+
20
+ _instruments = ("patchright >= 1.9.0",)
21
+
22
+ WRAPPED_METHODS = [
23
+ {
24
+ "package": "patchright.sync_api",
25
+ "object": "BrowserContext",
26
+ "method": "new_page",
27
+ "wrapper": _wrap_new_page,
28
+ },
29
+ {
30
+ "package": "patchright.sync_api",
31
+ "object": "Browser",
32
+ "method": "new_page",
33
+ "wrapper": _wrap_new_page,
34
+ },
35
+ {
36
+ "package": "patchright.sync_api",
37
+ "object": "BrowserType",
38
+ "method": "launch",
39
+ "wrapper": _wrap_new_browser_sync,
40
+ },
41
+ {
42
+ "package": "patchright.sync_api",
43
+ "object": "BrowserType",
44
+ "method": "connect",
45
+ "wrapper": _wrap_new_browser_sync,
46
+ },
47
+ {
48
+ "package": "patchright.sync_api",
49
+ "object": "BrowserType",
50
+ "method": "connect_over_cdp",
51
+ "wrapper": _wrap_new_browser_sync,
52
+ },
53
+ {
54
+ "package": "patchright.sync_api",
55
+ "object": "Browser",
56
+ "method": "close",
57
+ "wrapper": _wrap_close_browser_sync,
58
+ },
59
+ {
60
+ "package": "patchright.sync_api",
61
+ "object": "Browser",
62
+ "method": "new_context",
63
+ "wrapper": _wrap_new_context_sync,
64
+ },
65
+ {
66
+ "package": "patchright.sync_api",
67
+ "object": "BrowserType",
68
+ "method": "launch_persistent_context",
69
+ "wrapper": _wrap_new_context_sync,
70
+ },
71
+ ]
72
+
73
+ WRAPPED_METHODS_ASYNC = [
74
+ {
75
+ "package": "patchright.async_api",
76
+ "object": "BrowserContext",
77
+ "method": "new_page",
78
+ "wrapper": _wrap_new_page_async,
79
+ },
80
+ {
81
+ "package": "patchright.async_api",
82
+ "object": "Browser",
83
+ "method": "new_page",
84
+ "wrapper": _wrap_new_page_async,
85
+ },
86
+ {
87
+ "package": "patchright.async_api",
88
+ "object": "BrowserType",
89
+ "method": "launch",
90
+ "wrapper": _wrap_new_browser_async,
91
+ },
92
+ {
93
+ "package": "patchright.async_api",
94
+ "object": "BrowserType",
95
+ "method": "connect",
96
+ "wrapper": _wrap_new_browser_async,
97
+ },
98
+ {
99
+ "package": "patchright.async_api",
100
+ "object": "BrowserType",
101
+ "method": "connect_over_cdp",
102
+ "wrapper": _wrap_new_browser_async,
103
+ },
104
+ {
105
+ "package": "patchright.async_api",
106
+ "object": "Browser",
107
+ "method": "close",
108
+ "wrapper": _wrap_close_browser_async,
109
+ },
110
+ {
111
+ "package": "patchright.async_api",
112
+ "object": "Browser",
113
+ "method": "new_context",
114
+ "wrapper": _wrap_new_context_async,
115
+ },
116
+ {
117
+ "package": "patchright.async_api",
118
+ "object": "BrowserType",
119
+ "method": "launch_persistent_context",
120
+ "wrapper": _wrap_new_context_async,
121
+ },
122
+ ]
123
+
124
+
125
+ class PatchrightInstrumentor(BaseInstrumentor):
126
+ def __init__(self, client: LaminarClient, async_client: AsyncLaminarClient):
127
+ super().__init__()
128
+ self.client = client
129
+ self.async_client = async_client
130
+
131
+ def instrumentation_dependencies(self) -> Collection[str]:
132
+ return _instruments
133
+
134
+ def _instrument(self, **kwargs):
135
+ tracer_provider = kwargs.get("tracer_provider")
136
+ tracer = get_tracer(__name__, __version__, tracer_provider)
137
+
138
+ for wrapped_method in WRAPPED_METHODS:
139
+ wrap_package = wrapped_method.get("package")
140
+ wrap_object = wrapped_method.get("object")
141
+ wrap_method = wrapped_method.get("method")
142
+ try:
143
+ wrap_function_wrapper(
144
+ wrap_package,
145
+ f"{wrap_object}.{wrap_method}",
146
+ wrapped_method.get("wrapper")(
147
+ tracer,
148
+ self.client,
149
+ wrapped_method,
150
+ ),
151
+ )
152
+ except ModuleNotFoundError:
153
+ pass
154
+
155
+ for wrapped_method in WRAPPED_METHODS_ASYNC:
156
+ wrap_package = wrapped_method.get("package")
157
+ wrap_object = wrapped_method.get("object")
158
+ wrap_method = wrapped_method.get("method")
159
+ try:
160
+ wrap_function_wrapper(
161
+ wrap_package,
162
+ f"{wrap_object}.{wrap_method}",
163
+ wrapped_method.get("wrapper")(
164
+ tracer,
165
+ self.async_client,
166
+ wrapped_method,
167
+ ),
168
+ )
169
+ except ModuleNotFoundError:
170
+ pass
171
+
172
+ def _uninstrument(self, **kwargs):
173
+ for wrapped_method in WRAPPED_METHODS + WRAPPED_METHODS_ASYNC:
174
+ wrap_package = wrapped_method.get("package")
175
+ wrap_object = wrapped_method.get("object")
176
+ wrap_method = wrapped_method.get("method")
177
+ unwrap(wrap_package, f"{wrap_object}.{wrap_method}")
@@ -22,10 +22,11 @@ from typing import Collection
22
22
  from wrapt import wrap_function_wrapper
23
23
 
24
24
  try:
25
- from playwright.async_api import Browser, BrowserContext
25
+ from playwright.async_api import Browser, BrowserContext, Page
26
26
  from playwright.sync_api import (
27
27
  Browser as SyncBrowser,
28
28
  BrowserContext as SyncBrowserContext,
29
+ Page as SyncPage,
29
30
  )
30
31
  except ImportError as e:
31
32
  raise ImportError(
@@ -88,10 +89,15 @@ def _wrap_new_browser_sync(
88
89
  _context_spans[id(context)] = span
89
90
  span.set_attribute("lmnr.internal.has_browser_session", True)
90
91
  trace_id = format(span.get_span_context().trace_id, "032x")
92
+
93
+ def handle_page_navigation(page: SyncPage):
94
+ return handle_navigation_sync(page, session_id, trace_id, client)
95
+
91
96
  context.on(
92
97
  "page",
93
- lambda page: handle_navigation_sync(page, session_id, trace_id, client),
98
+ handle_page_navigation,
94
99
  )
100
+
95
101
  for page in context.pages:
96
102
  handle_navigation_sync(page, session_id, trace_id, client)
97
103
  return browser
@@ -115,7 +121,7 @@ async def _wrap_new_browser_async(
115
121
  span.set_attribute("lmnr.internal.has_browser_session", True)
116
122
  trace_id = format(span.get_span_context().trace_id, "032x")
117
123
 
118
- async def handle_page_navigation(page):
124
+ async def handle_page_navigation(page: Page):
119
125
  return await handle_navigation_async(page, session_id, trace_id, client)
120
126
 
121
127
  context.on("page", handle_page_navigation)
@@ -140,9 +146,12 @@ def _wrap_new_context_sync(
140
146
  span.set_attribute("lmnr.internal.has_browser_session", True)
141
147
  trace_id = format(span.get_span_context().trace_id, "032x")
142
148
 
149
+ def handle_page_navigation(page: SyncPage):
150
+ return handle_navigation_sync(page, session_id, trace_id, client)
151
+
143
152
  context.on(
144
153
  "page",
145
- lambda page: handle_navigation_sync(page, session_id, trace_id, client),
154
+ handle_page_navigation,
146
155
  )
147
156
  for page in context.pages:
148
157
  handle_navigation_sync(page, session_id, trace_id, client)
@@ -153,7 +162,7 @@ def _wrap_new_context_sync(
153
162
  async def _wrap_new_context_async(
154
163
  tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
155
164
  ):
156
- context: SyncBrowserContext = await wrapped(*args, **kwargs)
165
+ context: BrowserContext = await wrapped(*args, **kwargs)
157
166
  session_id = str(uuid.uuid4().hex)
158
167
  span = get_current_span()
159
168
  if span == INVALID_SPAN:
@@ -311,10 +320,10 @@ WRAPPED_METHODS_ASYNC = [
311
320
  "wrapper": _wrap_new_context_async,
312
321
  },
313
322
  {
314
- "package": "playwright.sync_api",
323
+ "package": "playwright.async_api",
315
324
  "object": "BrowserType",
316
325
  "method": "launch_persistent_context",
317
- "wrapper": _wrap_new_context_sync,
326
+ "wrapper": _wrap_new_context_async,
318
327
  },
319
328
  ]
320
329