lmnr 0.7.10__tar.gz → 0.7.11__tar.gz

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 (100) hide show
  1. {lmnr-0.7.10 → lmnr-0.7.11}/PKG-INFO +1 -1
  2. {lmnr-0.7.10 → lmnr-0.7.11}/pyproject.toml +1 -1
  3. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +36 -3
  4. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/tracing/instruments.py +4 -0
  5. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/browser/browser_use_cdp_otel.py +12 -7
  6. lmnr-0.7.11/src/lmnr/sdk/browser/bubus_otel.py +71 -0
  7. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/browser/cdp_utils.py +318 -87
  8. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/evaluations.py +22 -2
  9. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/laminar.py +3 -3
  10. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/version.py +1 -1
  11. {lmnr-0.7.10 → lmnr-0.7.11}/README.md +0 -0
  12. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/__init__.py +0 -0
  13. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/cli.py +0 -0
  14. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/.flake8 +0 -0
  15. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/__init__.py +0 -0
  16. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/decorators/__init__.py +0 -0
  17. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/litellm/__init__.py +0 -0
  18. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/litellm/utils.py +0 -0
  19. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +0 -0
  20. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +0 -0
  21. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +0 -0
  22. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +0 -0
  23. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +0 -0
  24. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +0 -0
  25. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +0 -0
  26. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +0 -0
  27. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +0 -0
  28. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py +0 -0
  29. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +0 -0
  30. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +0 -0
  31. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +0 -0
  32. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +0 -0
  33. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +0 -0
  34. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +0 -0
  35. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +0 -0
  36. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +0 -0
  37. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +0 -0
  38. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +0 -0
  39. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/utils.py +0 -0
  40. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +0 -0
  41. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +0 -0
  42. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +0 -0
  43. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +0 -0
  44. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +0 -0
  45. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +0 -0
  46. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +0 -0
  47. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +0 -0
  48. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +0 -0
  49. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +0 -0
  50. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +0 -0
  51. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +0 -0
  52. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +0 -0
  53. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +0 -0
  54. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +0 -0
  55. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +0 -0
  56. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +0 -0
  57. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +0 -0
  58. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +0 -0
  59. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +0 -0
  60. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/tracing/__init__.py +0 -0
  61. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/tracing/attributes.py +0 -0
  62. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/tracing/context.py +0 -0
  63. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/tracing/exporter.py +0 -0
  64. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/tracing/processor.py +0 -0
  65. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/tracing/tracer.py +0 -0
  66. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/utils/__init__.py +0 -0
  67. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/utils/json_encoder.py +0 -0
  68. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/utils/package_check.py +0 -0
  69. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/opentelemetry_lib/utils/wrappers.py +0 -0
  70. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/py.typed +0 -0
  71. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/__init__.py +0 -0
  72. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/browser/__init__.py +0 -0
  73. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/browser/browser_use_otel.py +0 -0
  74. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/browser/patchright_otel.py +0 -0
  75. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/browser/playwright_otel.py +0 -0
  76. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/browser/pw_utils.py +0 -0
  77. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/browser/recorder/record.umd.min.cjs +0 -0
  78. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/browser/utils.py +0 -0
  79. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/asynchronous/async_client.py +0 -0
  80. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/asynchronous/resources/__init__.py +0 -0
  81. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/asynchronous/resources/agent.py +0 -0
  82. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/asynchronous/resources/base.py +0 -0
  83. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/asynchronous/resources/browser_events.py +0 -0
  84. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/asynchronous/resources/evals.py +0 -0
  85. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/asynchronous/resources/evaluators.py +0 -0
  86. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/asynchronous/resources/tags.py +0 -0
  87. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/synchronous/resources/__init__.py +0 -0
  88. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/synchronous/resources/agent.py +0 -0
  89. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/synchronous/resources/base.py +0 -0
  90. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/synchronous/resources/browser_events.py +0 -0
  91. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/synchronous/resources/evals.py +0 -0
  92. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/synchronous/resources/evaluators.py +0 -0
  93. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/synchronous/resources/tags.py +0 -0
  94. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/client/synchronous/sync_client.py +0 -0
  95. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/datasets.py +0 -0
  96. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/decorators.py +0 -0
  97. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/eval_control.py +0 -0
  98. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/log.py +0 -0
  99. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/types.py +0 -0
  100. {lmnr-0.7.10 → lmnr-0.7.11}/src/lmnr/sdk/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lmnr
3
- Version: 0.7.10
3
+ Version: 0.7.11
4
4
  Summary: Python SDK for Laminar
5
5
  Author: lmnr.ai
6
6
  Author-email: lmnr.ai <founders@lmnr.ai>
@@ -6,7 +6,7 @@
6
6
 
7
7
  [project]
8
8
  name = "lmnr"
9
- version = "0.7.10"
9
+ version = "0.7.11"
10
10
  description = "Python SDK for Laminar"
11
11
  authors = [
12
12
  { name = "lmnr.ai", email = "founders@lmnr.ai" }
@@ -51,9 +51,19 @@ class BedrockInstrumentorInitializer(InstrumentorInitializer):
51
51
 
52
52
 
53
53
  class BrowserUseInstrumentorInitializer(InstrumentorInitializer):
54
- def init_instrumentor(
55
- self, client, async_client, *args, **kwargs
56
- ) -> BaseInstrumentor | None:
54
+ """Instruments for different versions of browser-use:
55
+
56
+ - browser-use < 0.5: BrowserUseLegacyInstrumentor to track agent_step and
57
+ other structure spans. Session instrumentation is controlled by
58
+ Instruments.PLAYWRIGHT (or Instruments.PATCHRIGHT for several versions
59
+ in 0.4.* that used patchright)
60
+ - browser-use ~= 0.5: Structure spans live in browser_use package itself.
61
+ Session instrumentation is controlled by Instruments.PLAYWRIGHT
62
+ - browser-use >= 0.6.0rc1: BubusInstrumentor to keep spans structure.
63
+ Session instrumentation is controlled by Instruments.BROWSER_USE_SESSION
64
+ """
65
+
66
+ def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
57
67
  if not is_package_installed("browser-use"):
58
68
  return None
59
69
 
@@ -65,6 +75,19 @@ class BrowserUseInstrumentorInitializer(InstrumentorInitializer):
65
75
 
66
76
  return BrowserUseLegacyInstrumentor()
67
77
 
78
+ return None
79
+
80
+
81
+ class BrowserUseSessionInstrumentorInitializer(InstrumentorInitializer):
82
+ def init_instrumentor(
83
+ self, client, async_client, *args, **kwargs
84
+ ) -> BaseInstrumentor | None:
85
+ if not is_package_installed("browser-use"):
86
+ return None
87
+
88
+ version = get_package_version("browser-use")
89
+ from packaging.version import parse
90
+
68
91
  if version and parse(version) >= parse("0.6.0rc1"):
69
92
  from lmnr.sdk.browser.browser_use_cdp_otel import BrowserUseInstrumentor
70
93
 
@@ -73,6 +96,16 @@ class BrowserUseInstrumentorInitializer(InstrumentorInitializer):
73
96
  return None
74
97
 
75
98
 
99
+ class BubusInstrumentorInitializer(InstrumentorInitializer):
100
+ def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
101
+ if not is_package_installed("bubus"):
102
+ return None
103
+
104
+ from lmnr.sdk.browser.bubus_otel import BubusInstrumentor
105
+
106
+ return BubusInstrumentor()
107
+
108
+
76
109
  class ChromaInstrumentorInitializer(InstrumentorInitializer):
77
110
  def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
78
111
  if not is_package_installed("chromadb"):
@@ -17,6 +17,8 @@ class Instruments(Enum):
17
17
  ANTHROPIC = "anthropic"
18
18
  BEDROCK = "bedrock"
19
19
  BROWSER_USE = "browser_use"
20
+ BROWSER_USE_SESSION = "browser_use_session"
21
+ BUBUS = "bubus"
20
22
  CHROMA = "chroma"
21
23
  COHERE = "cohere"
22
24
  CREWAI = "crewai"
@@ -60,6 +62,8 @@ INSTRUMENTATION_INITIALIZERS: dict[
60
62
  Instruments.ANTHROPIC: initializers.AnthropicInstrumentorInitializer(),
61
63
  Instruments.BEDROCK: initializers.BedrockInstrumentorInitializer(),
62
64
  Instruments.BROWSER_USE: initializers.BrowserUseInstrumentorInitializer(),
65
+ Instruments.BROWSER_USE_SESSION: initializers.BrowserUseSessionInstrumentorInitializer(),
66
+ Instruments.BUBUS: initializers.BubusInstrumentorInitializer(),
63
67
  Instruments.CHROMA: initializers.ChromaInstrumentorInitializer(),
64
68
  Instruments.COHERE: initializers.CohereInstrumentorInitializer(),
65
69
  Instruments.CREWAI: initializers.CrewAIInstrumentorInitializer(),
@@ -1,3 +1,6 @@
1
+ import asyncio
2
+ import uuid
3
+
1
4
  from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
2
5
  from lmnr.sdk.browser.utils import with_tracer_and_client_wrapper
3
6
  from lmnr.version import __version__
@@ -12,7 +15,6 @@ from opentelemetry.instrumentation.utils import unwrap
12
15
  from opentelemetry.trace import get_tracer, Tracer
13
16
  from typing import Collection
14
17
  from wrapt import wrap_function_wrapper
15
- import uuid
16
18
 
17
19
  # Stable versions, e.g. 0.6.0, satisfy this condition too
18
20
  _instruments = ("browser-use >= 0.6.0rc1",)
@@ -33,12 +35,7 @@ WRAPPED_METHODS = [
33
35
  ]
34
36
 
35
37
 
36
- @with_tracer_and_client_wrapper
37
- async def _wrap(
38
- tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
39
- ):
40
- result = await wrapped(*args, **kwargs)
41
-
38
+ async def process_wrapped_result(result, instance, client, to_wrap):
42
39
  if to_wrap.get("action") == "inject_session_recorder":
43
40
  is_registered = await is_recorder_present(result)
44
41
  if not is_registered:
@@ -50,6 +47,14 @@ async def _wrap(
50
47
  cdp_session = await instance.get_or_create_cdp_session(target_id)
51
48
  await take_full_snapshot(cdp_session)
52
49
 
50
+
51
+ @with_tracer_and_client_wrapper
52
+ async def _wrap(
53
+ tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
54
+ ):
55
+ result = await wrapped(*args, **kwargs)
56
+ asyncio.create_task(process_wrapped_result(result, instance, client, to_wrap))
57
+
53
58
  return result
54
59
 
55
60
 
@@ -0,0 +1,71 @@
1
+ from typing import Collection
2
+
3
+ from lmnr import Laminar
4
+ from lmnr.opentelemetry_lib.tracing.context import get_current_context
5
+ from lmnr.sdk.log import get_default_logger
6
+
7
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
8
+ from opentelemetry.instrumentation.utils import unwrap
9
+ from opentelemetry.trace import NonRecordingSpan, get_current_span
10
+ from wrapt import wrap_function_wrapper
11
+
12
+
13
+ _instruments = ("bubus >= 1.3.0",)
14
+ event_id_to_span_context = {}
15
+ logger = get_default_logger(__name__)
16
+
17
+
18
+ def wrap_dispatch(wrapped, instance, args, kwargs):
19
+ event = args[0] if args and len(args) > 0 else kwargs.get("event", None)
20
+ if event and hasattr(event, "event_id"):
21
+ event_id = event.event_id
22
+ if event_id:
23
+ span = get_current_span(get_current_context())
24
+ event_id_to_span_context[event_id] = span.get_span_context()
25
+ return wrapped(*args, **kwargs)
26
+
27
+
28
+ async def wrap_process_event(wrapped, instance, args, kwargs):
29
+ event = args[0] if args and len(args) > 0 else kwargs.get("event", None)
30
+ span_context = None
31
+ if event and hasattr(event, "event_id"):
32
+ event_id = event.event_id
33
+ if event_id:
34
+ span_context = event_id_to_span_context.get(event_id)
35
+ if not span_context:
36
+ return await wrapped(*args, **kwargs)
37
+ if not Laminar.is_initialized():
38
+ return await wrapped(*args, **kwargs)
39
+ with Laminar.use_span(NonRecordingSpan(span_context)):
40
+ return await wrapped(*args, **kwargs)
41
+
42
+
43
+ class BubusInstrumentor(BaseInstrumentor):
44
+ def __init__(self):
45
+ super().__init__()
46
+
47
+ def instrumentation_dependencies(self) -> Collection[str]:
48
+ return _instruments
49
+
50
+ def _instrument(self, **kwargs):
51
+ try:
52
+ wrap_function_wrapper("bubus.service", "EventBus.dispatch", wrap_dispatch)
53
+ except (ModuleNotFoundError, ImportError):
54
+ pass
55
+ try:
56
+ wrap_function_wrapper(
57
+ "bubus.service", "EventBus.process_event", wrap_process_event
58
+ )
59
+ except (ModuleNotFoundError, ImportError):
60
+ pass
61
+
62
+ def _uninstrument(self, **kwargs):
63
+ try:
64
+ unwrap("bubus.service", "EventBus.dispatch")
65
+ except (ModuleNotFoundError, ImportError):
66
+ pass
67
+ try:
68
+ unwrap("bubus.service", "EventBus.process_event")
69
+ except (ModuleNotFoundError, ImportError):
70
+ pass
71
+ event_id_to_span_context.clear()
@@ -1,8 +1,8 @@
1
- import orjson
1
+ import asyncio
2
2
  import logging
3
+ import orjson
3
4
  import os
4
5
  import time
5
- import asyncio
6
6
 
7
7
  from opentelemetry import trace
8
8
 
@@ -16,6 +16,11 @@ from lmnr.sdk.types import MaskInputOptions
16
16
  logger = logging.getLogger(__name__)
17
17
 
18
18
  OLD_BUFFER_TIMEOUT = 60
19
+ CDP_OPERATION_TIMEOUT_SECONDS = 10
20
+
21
+ # CDP ContextId is int
22
+ frame_to_isolated_context_id: dict[str, int] = {}
23
+ lock = asyncio.Lock()
19
24
 
20
25
  current_dir = os.path.dirname(os.path.abspath(__file__))
21
26
  with open(os.path.join(current_dir, "recorder", "record.umd.min.cjs"), "r") as f:
@@ -448,8 +453,6 @@ INJECT_PLACEHOLDER = """
448
453
  }
449
454
  }
450
455
 
451
- setInterval(sendBatchIfReady, BATCH_TIMEOUT);
452
-
453
456
  async function bufferToBase64(buffer) {
454
457
  const base64url = await new Promise(r => {
455
458
  const reader = new FileReader()
@@ -458,55 +461,152 @@ INJECT_PLACEHOLDER = """
458
461
  });
459
462
  return base64url.slice(base64url.indexOf(',') + 1);
460
463
  }
461
-
462
- window.lmnrRrweb.record({
463
- async emit(event) {
464
- try {
465
- const isLarge = isLargeEvent(event.type);
466
- const compressedResult = isLarge ?
467
- await compressLargeObject(event.data) :
468
- await compressSmallObject(event.data);
469
-
470
- const base64Data = await bufferToBase64(compressedResult);
471
- const eventToSend = {
472
- ...event,
473
- data: base64Data,
474
- };
475
- window.lmnrRrwebEventsBatch.push(eventToSend);
476
- } catch (error) {
477
- console.warn('Failed to push event to batch', error);
464
+
465
+ if (!window.lmnrStartedRecordingEvents) {
466
+ setInterval(sendBatchIfReady, BATCH_TIMEOUT);
467
+
468
+ window.lmnrRrweb.record({
469
+ async emit(event) {
470
+ try {
471
+ const isLarge = isLargeEvent(event.type);
472
+ const compressedResult = isLarge ?
473
+ await compressLargeObject(event.data) :
474
+ await compressSmallObject(event.data);
475
+
476
+ const base64Data = await bufferToBase64(compressedResult);
477
+ const eventToSend = {
478
+ ...event,
479
+ data: base64Data,
480
+ };
481
+ window.lmnrRrwebEventsBatch.push(eventToSend);
482
+ } catch (error) {
483
+ console.warn('Failed to push event to batch', error);
484
+ }
485
+ },
486
+ recordCanvas: true,
487
+ collectFonts: true,
488
+ recordCrossOriginIframes: true,
489
+ maskInputOptions: {
490
+ password: true,
491
+ textarea: maskInputOptions.textarea || false,
492
+ text: maskInputOptions.text || false,
493
+ number: maskInputOptions.number || false,
494
+ select: maskInputOptions.select || false,
495
+ email: maskInputOptions.email || false,
496
+ tel: maskInputOptions.tel || false,
478
497
  }
479
- },
480
- recordCanvas: true,
481
- collectFonts: true,
482
- recordCrossOriginIframes: true,
483
- maskInputOptions: {
484
- password: true,
485
- textarea: maskInputOptions.textarea || false,
486
- text: maskInputOptions.text || false,
487
- number: maskInputOptions.number || false,
488
- select: maskInputOptions.select || false,
489
- email: maskInputOptions.email || false,
490
- tel: maskInputOptions.tel || false,
498
+ });
499
+
500
+ function heartbeat() {
501
+ // Add heartbeat events
502
+ setInterval(() => {
503
+ window.lmnrRrweb.record.addCustomEvent('heartbeat', {
504
+ title: document.title,
505
+ url: document.URL,
506
+ })
507
+ }, HEARTBEAT_INTERVAL
508
+ );
491
509
  }
492
- });
493
-
494
- function heartbeat() {
495
- // Add heartbeat events
496
- setInterval(() => {
497
- window.lmnrRrweb.record.addCustomEvent('heartbeat', {
498
- title: document.title,
499
- url: document.URL,
500
- })
501
- }, HEARTBEAT_INTERVAL
502
- );
503
- }
504
510
 
505
- heartbeat();
511
+ heartbeat();
512
+ window.lmnrStartedRecordingEvents = true;
513
+ }
506
514
  }
507
515
  """
508
516
 
509
517
 
518
+ async def should_skip_page(cdp_session):
519
+ """Checks if the page url is an error page or an empty page.
520
+ This function returns True in case of any error in our code, because
521
+ it is safer to not record events than to try to inject the recorder
522
+ into something that is already broken.
523
+ """
524
+ cdp_client = cdp_session.cdp_client
525
+
526
+ try:
527
+ # Get the current page URL
528
+ result = await asyncio.wait_for(
529
+ cdp_client.send.Runtime.evaluate(
530
+ {
531
+ "expression": "window.location.href",
532
+ "returnByValue": True,
533
+ },
534
+ session_id=cdp_session.session_id,
535
+ ),
536
+ timeout=CDP_OPERATION_TIMEOUT_SECONDS,
537
+ )
538
+
539
+ url = result.get("result", {}).get("value", "")
540
+
541
+ # Comprehensive list of browser error URLs
542
+ error_url_patterns = [
543
+ "about:blank",
544
+ # Chrome error pages
545
+ "chrome-error://",
546
+ "chrome://network-error/",
547
+ "chrome://network-errors/",
548
+ # Chrome crash and debugging pages
549
+ "chrome://crash/",
550
+ "chrome://crashdump/",
551
+ "chrome://kill/",
552
+ "chrome://hang/",
553
+ "chrome://shorthang/",
554
+ "chrome://gpuclean/",
555
+ "chrome://gpucrash/",
556
+ "chrome://gpuhang/",
557
+ "chrome://memory-exhaust/",
558
+ "chrome://memory-pressure-critical/",
559
+ "chrome://memory-pressure-moderate/",
560
+ "chrome://inducebrowsercrashforrealz/",
561
+ "chrome://inducebrowserdcheckforrealz/",
562
+ "chrome://inducebrowserheapcorruption/",
563
+ "chrome://heapcorruptioncrash/",
564
+ "chrome://badcastcrash/",
565
+ "chrome://ppapiflashcrash/",
566
+ "chrome://ppapiflashhang/",
567
+ "chrome://quit/",
568
+ "chrome://restart/",
569
+ # Firefox error pages
570
+ "about:neterror",
571
+ "about:certerror",
572
+ "about:blocked",
573
+ # Firefox crash and debugging pages
574
+ "about:crashcontent",
575
+ "about:crashparent",
576
+ "about:crashes",
577
+ "about:tabcrashed",
578
+ # Edge error pages (similar to Chrome)
579
+ "edge-error://",
580
+ "edge://crash/",
581
+ "edge://kill/",
582
+ "edge://hang/",
583
+ # Safari/WebKit error indicators (data URLs with error content)
584
+ "webkit-error://",
585
+ ]
586
+
587
+ # Check if current URL matches any error pattern
588
+ if any(url.startswith(pattern) for pattern in error_url_patterns):
589
+ logger.debug(f"Detected browser error page from URL: {url}")
590
+ return True
591
+
592
+ # Additional check for data URLs that might contain error pages
593
+ if url.startswith("data:") and any(
594
+ error_term in url.lower()
595
+ for error_term in ["error", "crash", "failed", "unavailable", "not found"]
596
+ ):
597
+ logger.debug(f"Detected error page from data URL: {url[:100]}...")
598
+ return True
599
+
600
+ return False
601
+
602
+ except asyncio.TimeoutError:
603
+ logger.debug("Timeout error when checking if error page")
604
+ return True
605
+ except Exception as e:
606
+ logger.debug(f"Error during checking if error page: {e}")
607
+ return True
608
+
609
+
510
610
  def get_mask_input_setting() -> MaskInputOptions:
511
611
  """Get the mask_input setting from session recording configuration."""
512
612
  try:
@@ -534,12 +634,78 @@ def get_mask_input_setting() -> MaskInputOptions:
534
634
  )
535
635
 
536
636
 
537
- # browser_use.browser.session.CDPSession (browser-use >= 1.0.0)
538
- async def inject_session_recorder(cdp_session):
637
+ # browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
638
+ async def get_isolated_context_id(cdp_session) -> int | None:
639
+ async with lock:
640
+ tree = {}
641
+ try:
642
+ tree = await asyncio.wait_for(
643
+ cdp_session.cdp_client.send.Page.getFrameTree(
644
+ session_id=cdp_session.session_id
645
+ ),
646
+ timeout=CDP_OPERATION_TIMEOUT_SECONDS,
647
+ )
648
+ except asyncio.TimeoutError:
649
+ logger.debug("Timeout error when getting frame tree")
650
+ return None
651
+ except Exception as e:
652
+ logger.debug(f"Failed to get frame tree: {e}")
653
+ return None
654
+ frame = tree.get("frameTree", {}).get("frame", {})
655
+ frame_id = frame.get("id")
656
+ loader_id = frame.get("loaderId")
657
+
658
+ if frame_id is None or loader_id is None:
659
+ logger.debug("Failed to get frame id or loader id")
660
+ return None
661
+ key = f"{frame_id}_{loader_id}"
662
+
663
+ if key in frame_to_isolated_context_id:
664
+ return frame_to_isolated_context_id[key]
665
+
666
+ try:
667
+ result = await asyncio.wait_for(
668
+ cdp_session.cdp_client.send.Page.createIsolatedWorld(
669
+ {
670
+ "frameId": frame_id,
671
+ "worldName": "laminar-isolated-context",
672
+ },
673
+ session_id=cdp_session.session_id,
674
+ ),
675
+ timeout=CDP_OPERATION_TIMEOUT_SECONDS,
676
+ )
677
+ except asyncio.TimeoutError:
678
+ logger.debug("Timeout error when getting isolated context id")
679
+ return None
680
+ except Exception as e:
681
+ logger.debug(f"Failed to get isolated context id: {e}")
682
+ return None
683
+ isolated_context_id = result["executionContextId"]
684
+ frame_to_isolated_context_id[key] = isolated_context_id
685
+ return isolated_context_id
686
+
687
+
688
+ # browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
689
+ async def inject_session_recorder(cdp_session) -> int | None:
690
+ """Injects the session recorder base as well as the recorder itself.
691
+ Returns the isolated context id if successful.
692
+ """
693
+ isolated_context_id = None
539
694
  cdp_client = cdp_session.cdp_client
540
695
  try:
696
+ should_skip = True
541
697
  try:
542
- is_loaded = await is_recorder_present(cdp_session)
698
+ should_skip = await should_skip_page(cdp_session)
699
+ except Exception as e:
700
+ logger.debug(f"Failed to check if error page: {e}")
701
+
702
+ if should_skip:
703
+ logger.debug("Empty page detected, skipping session recorder injection")
704
+ return
705
+
706
+ isolated_context_id = await get_isolated_context_id(cdp_session)
707
+ try:
708
+ is_loaded = await is_recorder_present(cdp_session, isolated_context_id)
543
709
  except Exception as e:
544
710
  logger.debug(f"Failed to check if session recorder is loaded: {e}")
545
711
  is_loaded = False
@@ -547,42 +713,60 @@ async def inject_session_recorder(cdp_session):
547
713
  if is_loaded:
548
714
  return
549
715
 
716
+ if isolated_context_id is None:
717
+ logger.debug("Failed to get isolated context id")
718
+ return
719
+
550
720
  async def load_session_recorder():
551
721
  try:
552
- await cdp_client.send.Runtime.evaluate(
553
- {
554
- "expression": f"({RRWEB_CONTENT})()",
555
- "awaitPromise": True,
556
- },
557
- session_id=cdp_session.session_id,
722
+ await asyncio.wait_for(
723
+ cdp_client.send.Runtime.evaluate(
724
+ {
725
+ "expression": f"({RRWEB_CONTENT})()",
726
+ "contextId": isolated_context_id,
727
+ },
728
+ session_id=cdp_session.session_id,
729
+ ),
730
+ timeout=CDP_OPERATION_TIMEOUT_SECONDS,
558
731
  )
559
732
  return True
733
+ except asyncio.TimeoutError:
734
+ logger.debug("Timeout error when loading session recorder base")
735
+ return False
560
736
  except Exception as e:
561
- logger.error(f"Failed to load session recorder: {e}")
737
+ logger.debug(f"Failed to load session recorder base: {e}")
562
738
  return False
563
739
 
564
740
  if not await retry_async(
565
741
  load_session_recorder,
742
+ retries=3,
566
743
  delay=1,
567
744
  error_message="Failed to load session recorder",
568
745
  ):
569
746
  return
570
747
 
571
748
  try:
572
- await cdp_client.send.Runtime.evaluate(
573
- {
574
- "expression": f"({INJECT_PLACEHOLDER})({orjson.dumps(get_mask_input_setting()).decode("utf-8")})",
575
- },
576
- session_id=cdp_session.session_id,
749
+ await asyncio.wait_for(
750
+ cdp_client.send.Runtime.evaluate(
751
+ {
752
+ "expression": f"({INJECT_PLACEHOLDER})({orjson.dumps(get_mask_input_setting()).decode('utf-8')})",
753
+ "contextId": isolated_context_id,
754
+ },
755
+ session_id=cdp_session.session_id,
756
+ ),
757
+ timeout=CDP_OPERATION_TIMEOUT_SECONDS,
577
758
  )
759
+ return isolated_context_id
760
+ except asyncio.TimeoutError:
761
+ logger.debug("Timeout error when injecting session recorder")
578
762
  except Exception as e:
579
- logger.debug(f"Failed to inject session recorder placeholder: {e}")
763
+ logger.debug(f"Failed to inject recorder: {e}")
580
764
 
581
765
  except Exception as e:
582
- logger.error(f"Error during session recorder injection: {e}")
766
+ logger.debug(f"Error during session recorder injection: {e}")
583
767
 
584
768
 
585
- # browser_use.browser.session.CDPSession (browser-use >= 1.0.0)
769
+ # browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
586
770
  @observe(name="cdp_use.session", ignore_input=True, ignore_output=True)
587
771
  async def start_recording_events(
588
772
  cdp_session,
@@ -596,7 +780,10 @@ async def start_recording_events(
596
780
  trace_id = format(span.get_span_context().trace_id, "032x")
597
781
  span.set_attribute("lmnr.internal.has_browser_session", True)
598
782
 
599
- await inject_session_recorder(cdp_session)
783
+ isolated_context_id = await inject_session_recorder(cdp_session)
784
+ if isolated_context_id is None:
785
+ logger.debug("Failed to inject session recorder, not registering bindings")
786
+ return
600
787
 
601
788
  # Buffer for reassembling chunks
602
789
  chunk_buffers = {}
@@ -654,11 +841,14 @@ async def start_recording_events(
654
841
  async def send_events_callback(event, cdp_session_id: str | None = None):
655
842
  if event["name"] != "lmnrSendEvents":
656
843
  return
844
+ if event["executionContextId"] != isolated_context_id:
845
+ return
657
846
  await send_events_from_browser(orjson.loads(event["payload"]))
658
847
 
659
848
  await cdp_client.send.Runtime.addBinding(
660
849
  {
661
850
  "name": "lmnrSendEvents",
851
+ "executionContextId": isolated_context_id,
662
852
  },
663
853
  session_id=cdp_session.session_id,
664
854
  )
@@ -668,7 +858,7 @@ async def start_recording_events(
668
858
  register_on_target_created(cdp_session, lmnr_session_id, client)
669
859
 
670
860
 
671
- # browser_use.browser.session.CDPSession (browser-use >= 1.0.0)
861
+ # browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
672
862
  async def enable_target_discovery(cdp_session):
673
863
  cdp_client = cdp_session.cdp_client
674
864
  await cdp_client.send.Target.setDiscoverTargets(
@@ -679,7 +869,7 @@ async def enable_target_discovery(cdp_session):
679
869
  )
680
870
 
681
871
 
682
- # browser_use.browser.session.CDPSession (browser-use >= 1.0.0)
872
+ # browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
683
873
  def register_on_target_created(
684
874
  cdp_session, lmnr_session_id: str, client: AsyncLaminarClient
685
875
  ):
@@ -689,31 +879,63 @@ def register_on_target_created(
689
879
  if target_info["type"] == "page":
690
880
  asyncio.create_task(inject_session_recorder(cdp_session=cdp_session))
691
881
 
692
- cdp_session.cdp_client.register.Target.targetCreated(on_target_created)
882
+ try:
883
+ cdp_session.cdp_client.register.Target.targetCreated(on_target_created)
884
+ except Exception as e:
885
+ logger.debug(f"Failed to register on target created: {e}")
693
886
 
694
887
 
695
- # browser_use.browser.session.CDPSession (browser-use >= 1.0.0)
696
- async def is_recorder_present(cdp_session) -> bool:
888
+ # browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
889
+ async def is_recorder_present(
890
+ cdp_session, isolated_context_id: int | None = None
891
+ ) -> bool:
892
+ # This function returns True on any error, because it is safer to not record
893
+ # events than to try to inject the recorder into a broken context.
697
894
  cdp_client = cdp_session.cdp_client
895
+ if isolated_context_id is None:
896
+ isolated_context_id = await get_isolated_context_id(cdp_session)
897
+ if isolated_context_id is None:
898
+ logger.debug("Failed to get isolated context id")
899
+ return True
698
900
 
699
- result = await cdp_client.send.Runtime.evaluate(
700
- {
701
- "expression": """(()=>{
702
- return typeof window.lmnrRrweb !== 'undefined';
703
- })()""",
704
- },
705
- session_id=cdp_session.session_id,
706
- )
707
- if result and "result" in result and "value" in result["result"]:
708
- return result["result"]["value"]
709
- return False
901
+ try:
902
+ result = await asyncio.wait_for(
903
+ cdp_client.send.Runtime.evaluate(
904
+ {
905
+ "expression": "typeof window.lmnrRrweb !== 'undefined'",
906
+ "contextId": isolated_context_id,
907
+ },
908
+ session_id=cdp_session.session_id,
909
+ ),
910
+ timeout=CDP_OPERATION_TIMEOUT_SECONDS,
911
+ )
912
+ if result and "result" in result and "value" in result["result"]:
913
+ return result["result"]["value"]
914
+ return False
915
+ except asyncio.TimeoutError:
916
+ logger.debug("Timeout error when checking if session recorder is present")
917
+ return True
918
+ except Exception:
919
+ logger.debug("Exception when checking if session recorder is present")
920
+ return True
710
921
 
711
922
 
712
923
  async def take_full_snapshot(cdp_session):
713
924
  cdp_client = cdp_session.cdp_client
714
- result = await cdp_client.send.Runtime.evaluate(
715
- {
716
- "expression": """(() => {
925
+ isolated_context_id = await get_isolated_context_id(cdp_session)
926
+ if isolated_context_id is None:
927
+ logger.debug("Failed to get isolated context id")
928
+ return False
929
+
930
+ if await should_skip_page(cdp_session):
931
+ logger.debug("Skipping full snapshot")
932
+ return False
933
+
934
+ try:
935
+ result = await asyncio.wait_for(
936
+ cdp_client.send.Runtime.evaluate(
937
+ {
938
+ "expression": """(() => {
717
939
  if (window.lmnrRrweb) {
718
940
  try {
719
941
  window.lmnrRrweb.record.takeFullSnapshot();
@@ -725,9 +947,18 @@ async def take_full_snapshot(cdp_session):
725
947
  }
726
948
  return false;
727
949
  })()""",
728
- },
729
- session_id=cdp_session.session_id,
730
- )
950
+ "contextId": isolated_context_id,
951
+ },
952
+ session_id=cdp_session.session_id,
953
+ ),
954
+ timeout=CDP_OPERATION_TIMEOUT_SECONDS,
955
+ )
956
+ except asyncio.TimeoutError:
957
+ logger.debug("Timeout error when taking full snapshot")
958
+ return False
959
+ except Exception as e:
960
+ logger.debug(f"Error when taking full snapshot: {e}")
961
+ return False
731
962
  if result and "result" in result and "value" in result["result"]:
732
963
  return result["result"]["value"]
733
964
  return False
@@ -112,7 +112,12 @@ class Evaluation:
112
112
  base_http_url: str | None = None,
113
113
  http_port: int | None = None,
114
114
  grpc_port: int | None = None,
115
- instruments: set[Instruments] | None = None,
115
+ instruments: (
116
+ set[Instruments] | list[Instruments] | tuple[Instruments] | None
117
+ ) = None,
118
+ disabled_instruments: (
119
+ set[Instruments] | list[Instruments] | tuple[Instruments] | None
120
+ ) = None,
116
121
  max_export_batch_size: int | None = MAX_EXPORT_BATCH_SIZE,
117
122
  trace_export_timeout_seconds: int | None = None,
118
123
  ):
@@ -172,6 +177,10 @@ class Evaluation:
172
177
  used.
173
178
  See https://docs.lmnr.ai/tracing/automatic-instrumentation
174
179
  Defaults to None.
180
+ disabled_instruments (set[Instruments] | None, optional): Set of modules\
181
+ to disable auto-instrumentations. If None, only modules passed\
182
+ as `instruments` will be disabled.
183
+ Defaults to None.
175
184
  """
176
185
 
177
186
  if not evaluators:
@@ -234,6 +243,7 @@ class Evaluation:
234
243
  http_port=http_port,
235
244
  grpc_port=grpc_port,
236
245
  instruments=instruments,
246
+ disabled_instruments=disabled_instruments,
237
247
  max_export_batch_size=max_export_batch_size,
238
248
  export_timeout_seconds=trace_export_timeout_seconds,
239
249
  )
@@ -432,7 +442,12 @@ def evaluate(
432
442
  base_http_url: str | None = None,
433
443
  http_port: int | None = None,
434
444
  grpc_port: int | None = None,
435
- instruments: set[Instruments] | None = None,
445
+ instruments: (
446
+ set[Instruments] | list[Instruments] | tuple[Instruments] | None
447
+ ) = None,
448
+ disabled_instruments: (
449
+ set[Instruments] | list[Instruments] | tuple[Instruments] | None
450
+ ) = None,
436
451
  max_export_batch_size: int | None = MAX_EXPORT_BATCH_SIZE,
437
452
  trace_export_timeout_seconds: int | None = None,
438
453
  ) -> Awaitable[None] | None:
@@ -493,6 +508,10 @@ def evaluate(
493
508
  auto-instrument. If None, all available instruments\
494
509
  will be used.
495
510
  Defaults to None.
511
+ disabled_instruments (set[Instruments] | None, optional): Set of modules\
512
+ to disable auto-instrumentations. If None, no\
513
+ If None, only modules passed as `instruments` will be disabled.
514
+ Defaults to None.
496
515
  trace_export_timeout_seconds (int | None, optional): The timeout for\
497
516
  trace export on OpenTelemetry exporter. Defaults to None.
498
517
  """
@@ -510,6 +529,7 @@ def evaluate(
510
529
  http_port=http_port,
511
530
  grpc_port=grpc_port,
512
531
  instruments=instruments,
532
+ disabled_instruments=disabled_instruments,
513
533
  max_export_batch_size=max_export_batch_size,
514
534
  trace_export_timeout_seconds=trace_export_timeout_seconds,
515
535
  )
@@ -421,14 +421,14 @@ class Laminar:
421
421
 
422
422
  Usage example:
423
423
  ```python
424
- from src.lmnr import Laminar, use_span
424
+ from src.lmnr import Laminar
425
425
  def foo(span):
426
- with use_span(span):
426
+ with Laminar.use_span(span):
427
427
  with Laminar.start_as_current_span("foo_inner"):
428
428
  some_function()
429
429
 
430
430
  def bar():
431
- with use_span(span):
431
+ with Laminar.use_span(span):
432
432
  openai_client.chat.completions.create()
433
433
 
434
434
  span = Laminar.start_span("outer")
@@ -3,7 +3,7 @@ import httpx
3
3
  from packaging import version
4
4
 
5
5
 
6
- __version__ = "0.7.10"
6
+ __version__ = "0.7.11"
7
7
  PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
8
8
 
9
9
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes