lmnr 0.6.18__py3-none-any.whl → 0.6.20__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 (37) hide show
  1. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +55 -20
  2. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +23 -0
  3. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
  4. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +442 -0
  5. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1024 -0
  6. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +297 -0
  7. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
  8. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +308 -0
  9. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +185 -0
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +358 -0
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +319 -0
  16. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +132 -0
  17. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +626 -0
  18. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
  19. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +1 -3
  20. lmnr/sdk/browser/browser_use_otel.py +1 -1
  21. lmnr/sdk/browser/patchright_otel.py +0 -14
  22. lmnr/sdk/browser/playwright_otel.py +16 -130
  23. lmnr/sdk/browser/pw_utils.py +45 -31
  24. lmnr/sdk/client/asynchronous/async_client.py +13 -0
  25. lmnr/sdk/client/asynchronous/resources/__init__.py +2 -0
  26. lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
  27. lmnr/sdk/client/asynchronous/resources/tags.py +4 -10
  28. lmnr/sdk/client/synchronous/resources/__init__.py +2 -1
  29. lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
  30. lmnr/sdk/client/synchronous/resources/tags.py +4 -10
  31. lmnr/sdk/client/synchronous/sync_client.py +14 -0
  32. lmnr/sdk/utils.py +23 -0
  33. lmnr/version.py +1 -1
  34. {lmnr-0.6.18.dist-info → lmnr-0.6.20.dist-info}/METADATA +2 -5
  35. {lmnr-0.6.18.dist-info → lmnr-0.6.20.dist-info}/RECORD +37 -18
  36. {lmnr-0.6.18.dist-info → lmnr-0.6.20.dist-info}/WHEEL +1 -1
  37. {lmnr-0.6.18.dist-info → lmnr-0.6.20.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,100 @@
1
+ from dataclasses import asdict
2
+ from enum import Enum
3
+ from typing import Union
4
+
5
+ from opentelemetry._events import Event
6
+ from ..shared.event_models import (
7
+ ChoiceEvent,
8
+ MessageEvent,
9
+ )
10
+ from ..utils import (
11
+ should_emit_events,
12
+ should_send_prompts,
13
+ )
14
+ from opentelemetry.semconv._incubating.attributes import (
15
+ gen_ai_attributes as GenAIAttributes,
16
+ )
17
+
18
+ from .config import Config
19
+
20
+
21
+ class Roles(Enum):
22
+ USER = "user"
23
+ ASSISTANT = "assistant"
24
+ SYSTEM = "system"
25
+ TOOL = "tool"
26
+
27
+
28
+ VALID_MESSAGE_ROLES = {role.value for role in Roles}
29
+ """The valid roles for naming the message event."""
30
+
31
+ EVENT_ATTRIBUTES = {
32
+ GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.OPENAI.value
33
+ }
34
+ """The attributes to be used for the event."""
35
+
36
+
37
+ def emit_event(event: Union[MessageEvent, ChoiceEvent]) -> None:
38
+ """
39
+ Emit an event to the OpenTelemetry SDK.
40
+
41
+ Args:
42
+ event: The event to emit.
43
+ """
44
+ if not should_emit_events():
45
+ return
46
+
47
+ if isinstance(event, MessageEvent):
48
+ _emit_message_event(event)
49
+ elif isinstance(event, ChoiceEvent):
50
+ _emit_choice_event(event)
51
+ else:
52
+ raise TypeError("Unsupported event type")
53
+
54
+
55
+ def _emit_message_event(event: MessageEvent) -> None:
56
+ body = asdict(event)
57
+
58
+ if event.role in VALID_MESSAGE_ROLES:
59
+ name = "gen_ai.{}.message".format(event.role)
60
+ # According to the semantic conventions, the role is conditionally required if available
61
+ # and not equal to the "role" in the message name. So, remove the role from the body if
62
+ # it is the same as the in the event name.
63
+ body.pop("role", None)
64
+ else:
65
+ name = "gen_ai.user.message"
66
+
67
+ # According to the semantic conventions, only the assistant role has tool call
68
+ if event.role != Roles.ASSISTANT.value and event.tool_calls is not None:
69
+ del body["tool_calls"]
70
+ elif event.tool_calls is None:
71
+ del body["tool_calls"]
72
+
73
+ if not should_send_prompts():
74
+ del body["content"]
75
+ if body.get("tool_calls") is not None:
76
+ for tool_call in body["tool_calls"]:
77
+ tool_call["function"].pop("arguments", None)
78
+
79
+ Config.event_logger.emit(Event(name=name, body=body, attributes=EVENT_ATTRIBUTES))
80
+
81
+
82
+ def _emit_choice_event(event: ChoiceEvent) -> None:
83
+ body = asdict(event)
84
+ if event.message["role"] == Roles.ASSISTANT.value:
85
+ # According to the semantic conventions, the role is conditionally required if available
86
+ # and not equal to "assistant", so remove the role from the body if it is "assistant".
87
+ body["message"].pop("role", None)
88
+
89
+ if event.tool_calls is None:
90
+ del body["tool_calls"]
91
+
92
+ if not should_send_prompts():
93
+ body["message"].pop("content", None)
94
+ if body.get("tool_calls") is not None:
95
+ for tool_call in body["tool_calls"]:
96
+ tool_call["function"].pop("arguments", None)
97
+
98
+ Config.event_logger.emit(
99
+ Event(name="gen_ai.choice", body=body, attributes=EVENT_ATTRIBUTES)
100
+ )
@@ -0,0 +1,41 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, List, Literal, Optional, TypedDict
3
+
4
+
5
+ class _FunctionToolCall(TypedDict):
6
+ function_name: str
7
+ arguments: Optional[dict[str, Any]]
8
+
9
+
10
+ class ToolCall(TypedDict):
11
+ """Represents a tool call in the AI model."""
12
+
13
+ id: str
14
+ function: _FunctionToolCall
15
+ type: Literal["function"]
16
+
17
+
18
+ class CompletionMessage(TypedDict):
19
+ """Represents a message in the AI model."""
20
+
21
+ content: Any
22
+ role: str = "assistant"
23
+
24
+
25
+ @dataclass
26
+ class MessageEvent:
27
+ """Represents an input event for the AI model."""
28
+
29
+ content: Any
30
+ role: str = "user"
31
+ tool_calls: Optional[List[ToolCall]] = None
32
+
33
+
34
+ @dataclass
35
+ class ChoiceEvent:
36
+ """Represents a completion event for the AI model."""
37
+
38
+ index: int
39
+ message: CompletionMessage
40
+ finish_reason: str = "unknown"
41
+ tool_calls: Optional[List[ToolCall]] = None
@@ -0,0 +1,68 @@
1
+ import time
2
+
3
+ from opentelemetry import context as context_api
4
+ from ..utils import is_openai_v1
5
+ from ..shared import (
6
+ _get_openai_base_url,
7
+ metric_shared_attributes,
8
+ model_as_dict,
9
+ )
10
+ from ..utils import (
11
+ _with_image_gen_metric_wrapper,
12
+ )
13
+ from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
14
+ from opentelemetry.metrics import Counter, Histogram
15
+ from opentelemetry.semconv_ai import SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY
16
+
17
+
18
+ @_with_image_gen_metric_wrapper
19
+ def image_gen_metrics_wrapper(
20
+ duration_histogram: Histogram,
21
+ exception_counter: Counter,
22
+ wrapped,
23
+ instance,
24
+ args,
25
+ kwargs,
26
+ ):
27
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value(
28
+ SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY
29
+ ):
30
+ return wrapped(*args, **kwargs)
31
+
32
+ try:
33
+ # record time for duration
34
+ start_time = time.time()
35
+ response = wrapped(*args, **kwargs)
36
+ end_time = time.time()
37
+ except Exception as e: # pylint: disable=broad-except
38
+ end_time = time.time()
39
+ duration = end_time - start_time if "start_time" in locals() else 0
40
+
41
+ attributes = {
42
+ "error.type": e.__class__.__name__,
43
+ }
44
+
45
+ if duration > 0 and duration_histogram:
46
+ duration_histogram.record(duration, attributes=attributes)
47
+ if exception_counter:
48
+ exception_counter.add(1, attributes=attributes)
49
+
50
+ raise
51
+
52
+ if is_openai_v1():
53
+ response_dict = model_as_dict(response)
54
+ else:
55
+ response_dict = response
56
+
57
+ # not provide response.model in ImagesResponse response, use model in request kwargs
58
+ shared_attributes = metric_shared_attributes(
59
+ response_model=kwargs.get("model") or None,
60
+ operation="image_gen",
61
+ server_address=_get_openai_base_url(instance),
62
+ )
63
+
64
+ duration = end_time - start_time
65
+ if duration_histogram:
66
+ duration_histogram.record(duration, attributes=shared_attributes)
67
+
68
+ return response
@@ -0,0 +1,185 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import threading
5
+ import traceback
6
+ from contextlib import asynccontextmanager
7
+ from importlib.metadata import version
8
+
9
+ from opentelemetry import context as context_api
10
+ from opentelemetry._events import EventLogger
11
+ from .shared.config import Config
12
+
13
+ import openai
14
+
15
+ _OPENAI_VERSION = version("openai")
16
+
17
+ TRACELOOP_TRACE_CONTENT = "TRACELOOP_TRACE_CONTENT"
18
+
19
+
20
+ def is_openai_v1():
21
+ return _OPENAI_VERSION >= "1.0.0"
22
+
23
+
24
+ def is_azure_openai(instance):
25
+ return is_openai_v1() and isinstance(
26
+ instance._client, (openai.AsyncAzureOpenAI, openai.AzureOpenAI)
27
+ )
28
+
29
+
30
+ def is_metrics_enabled() -> bool:
31
+ return (os.getenv("TRACELOOP_METRICS_ENABLED") or "true").lower() == "true"
32
+
33
+
34
+ def should_record_stream_token_usage():
35
+ return Config.enrich_token_usage
36
+
37
+
38
+ def _with_image_gen_metric_wrapper(func):
39
+ def _with_metric(duration_histogram, exception_counter):
40
+ def wrapper(wrapped, instance, args, kwargs):
41
+ return func(
42
+ duration_histogram,
43
+ exception_counter,
44
+ wrapped,
45
+ instance,
46
+ args,
47
+ kwargs,
48
+ )
49
+
50
+ return wrapper
51
+
52
+ return _with_metric
53
+
54
+
55
+ def _with_embeddings_telemetry_wrapper(func):
56
+ def _with_embeddings_telemetry(
57
+ tracer,
58
+ token_counter,
59
+ vector_size_counter,
60
+ duration_histogram,
61
+ exception_counter,
62
+ ):
63
+ def wrapper(wrapped, instance, args, kwargs):
64
+ return func(
65
+ tracer,
66
+ token_counter,
67
+ vector_size_counter,
68
+ duration_histogram,
69
+ exception_counter,
70
+ wrapped,
71
+ instance,
72
+ args,
73
+ kwargs,
74
+ )
75
+
76
+ return wrapper
77
+
78
+ return _with_embeddings_telemetry
79
+
80
+
81
+ def _with_chat_telemetry_wrapper(func):
82
+ def _with_chat_telemetry(
83
+ tracer,
84
+ token_counter,
85
+ choice_counter,
86
+ duration_histogram,
87
+ exception_counter,
88
+ streaming_time_to_first_token,
89
+ streaming_time_to_generate,
90
+ ):
91
+ def wrapper(wrapped, instance, args, kwargs):
92
+ return func(
93
+ tracer,
94
+ token_counter,
95
+ choice_counter,
96
+ duration_histogram,
97
+ exception_counter,
98
+ streaming_time_to_first_token,
99
+ streaming_time_to_generate,
100
+ wrapped,
101
+ instance,
102
+ args,
103
+ kwargs,
104
+ )
105
+
106
+ return wrapper
107
+
108
+ return _with_chat_telemetry
109
+
110
+
111
+ def _with_tracer_wrapper(func):
112
+ def _with_tracer(tracer):
113
+ def wrapper(wrapped, instance, args, kwargs):
114
+ return func(tracer, wrapped, instance, args, kwargs)
115
+
116
+ return wrapper
117
+
118
+ return _with_tracer
119
+
120
+
121
+ @asynccontextmanager
122
+ async def start_as_current_span_async(tracer, *args, **kwargs):
123
+ with tracer.start_as_current_span(*args, **kwargs) as span:
124
+ yield span
125
+
126
+
127
+ def dont_throw(func):
128
+ """
129
+ A decorator that wraps the passed in function and logs exceptions instead of throwing them.
130
+ Works for both synchronous and asynchronous functions.
131
+ """
132
+ logger = logging.getLogger(func.__module__)
133
+
134
+ async def async_wrapper(*args, **kwargs):
135
+ try:
136
+ return await func(*args, **kwargs)
137
+ except Exception as e:
138
+ _handle_exception(e, func, logger)
139
+
140
+ def sync_wrapper(*args, **kwargs):
141
+ try:
142
+ return func(*args, **kwargs)
143
+ except Exception as e:
144
+ _handle_exception(e, func, logger)
145
+
146
+ def _handle_exception(e, func, logger):
147
+ logger.debug(
148
+ "OpenLLMetry failed to trace in %s, error: %s",
149
+ func.__name__,
150
+ traceback.format_exc(),
151
+ )
152
+ if Config.exception_logger:
153
+ Config.exception_logger(e)
154
+
155
+ return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
156
+
157
+
158
+ def run_async(method):
159
+ try:
160
+ loop = asyncio.get_running_loop()
161
+ except RuntimeError:
162
+ loop = None
163
+
164
+ if loop and loop.is_running():
165
+ thread = threading.Thread(target=lambda: asyncio.run(method))
166
+ thread.start()
167
+ thread.join()
168
+ else:
169
+ asyncio.run(method)
170
+
171
+
172
+ def should_send_prompts():
173
+ return (
174
+ os.getenv(TRACELOOP_TRACE_CONTENT) or "true"
175
+ ).lower() == "true" or context_api.get_value("override_enable_content_tracing")
176
+
177
+
178
+ def should_emit_events() -> bool:
179
+ """
180
+ Checks if the instrumentation isn't using the legacy attributes
181
+ and if the event logger is not None.
182
+ """
183
+ return not Config.use_legacy_attributes and isinstance(
184
+ Config.event_logger, EventLogger
185
+ )
@@ -0,0 +1,176 @@
1
+ from typing import Collection
2
+
3
+ from opentelemetry._events import get_event_logger
4
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
5
+ from ..shared.chat_wrappers import (
6
+ achat_wrapper,
7
+ chat_wrapper,
8
+ )
9
+ from ..shared.completion_wrappers import (
10
+ acompletion_wrapper,
11
+ completion_wrapper,
12
+ )
13
+ from ..shared.config import Config
14
+ from ..shared.embeddings_wrappers import (
15
+ aembeddings_wrapper,
16
+ embeddings_wrapper,
17
+ )
18
+ from ..utils import is_metrics_enabled
19
+ from ..version import __version__
20
+ from opentelemetry.instrumentation.utils import unwrap
21
+ from opentelemetry.metrics import get_meter
22
+ from opentelemetry.semconv._incubating.metrics import gen_ai_metrics as GenAIMetrics
23
+ from opentelemetry.semconv_ai import Meters
24
+ from opentelemetry.trace import get_tracer
25
+ from wrapt import wrap_function_wrapper
26
+
27
+ _instruments = ("openai >= 0.27.0", "openai < 1.0.0")
28
+
29
+
30
+ class OpenAIV0Instrumentor(BaseInstrumentor):
31
+ def instrumentation_dependencies(self) -> Collection[str]:
32
+ return _instruments
33
+
34
+ def _instrument(self, **kwargs):
35
+ tracer_provider = kwargs.get("tracer_provider")
36
+ tracer = get_tracer(__name__, __version__, tracer_provider)
37
+
38
+ meter_provider = kwargs.get("meter_provider")
39
+ meter = get_meter(__name__, __version__, meter_provider)
40
+
41
+ if not Config.use_legacy_attributes:
42
+ event_logger_provider = kwargs.get("event_logger_provider")
43
+ Config.event_logger = get_event_logger(
44
+ __name__, __version__, event_logger_provider=event_logger_provider
45
+ )
46
+
47
+ if is_metrics_enabled():
48
+ tokens_histogram = meter.create_histogram(
49
+ name=Meters.LLM_TOKEN_USAGE,
50
+ unit="token",
51
+ description="Measures number of input and output tokens used",
52
+ )
53
+
54
+ chat_choice_counter = meter.create_counter(
55
+ name=Meters.LLM_GENERATION_CHOICES,
56
+ unit="choice",
57
+ description="Number of choices returned by chat completions call",
58
+ )
59
+
60
+ duration_histogram = meter.create_histogram(
61
+ name=Meters.LLM_OPERATION_DURATION,
62
+ unit="s",
63
+ description="GenAI operation duration",
64
+ )
65
+
66
+ chat_exception_counter = meter.create_counter(
67
+ name=Meters.LLM_COMPLETIONS_EXCEPTIONS,
68
+ unit="time",
69
+ description="Number of exceptions occurred during chat completions",
70
+ )
71
+
72
+ streaming_time_to_first_token = meter.create_histogram(
73
+ name=GenAIMetrics.GEN_AI_SERVER_TIME_TO_FIRST_TOKEN,
74
+ unit="s",
75
+ description="Time to first token in streaming chat completions",
76
+ )
77
+ streaming_time_to_generate = meter.create_histogram(
78
+ name=Meters.LLM_STREAMING_TIME_TO_GENERATE,
79
+ unit="s",
80
+ description="Time between first token and completion in streaming chat completions",
81
+ )
82
+ else:
83
+ (
84
+ tokens_histogram,
85
+ chat_choice_counter,
86
+ duration_histogram,
87
+ chat_exception_counter,
88
+ streaming_time_to_first_token,
89
+ streaming_time_to_generate,
90
+ ) = (None, None, None, None, None, None)
91
+
92
+ if is_metrics_enabled():
93
+ embeddings_vector_size_counter = meter.create_counter(
94
+ name=Meters.LLM_EMBEDDINGS_VECTOR_SIZE,
95
+ unit="element",
96
+ description="he size of returned vector",
97
+ )
98
+ embeddings_exception_counter = meter.create_counter(
99
+ name=Meters.LLM_EMBEDDINGS_EXCEPTIONS,
100
+ unit="time",
101
+ description="Number of exceptions occurred during embeddings operation",
102
+ )
103
+ else:
104
+ (
105
+ tokens_histogram,
106
+ embeddings_vector_size_counter,
107
+ embeddings_exception_counter,
108
+ ) = (None, None, None)
109
+
110
+ wrap_function_wrapper(
111
+ "openai",
112
+ "Completion.create",
113
+ completion_wrapper(tracer),
114
+ )
115
+
116
+ wrap_function_wrapper(
117
+ "openai",
118
+ "Completion.acreate",
119
+ acompletion_wrapper(tracer),
120
+ )
121
+ wrap_function_wrapper(
122
+ "openai",
123
+ "ChatCompletion.create",
124
+ chat_wrapper(
125
+ tracer,
126
+ tokens_histogram,
127
+ chat_choice_counter,
128
+ duration_histogram,
129
+ chat_exception_counter,
130
+ streaming_time_to_first_token,
131
+ streaming_time_to_generate,
132
+ ),
133
+ )
134
+ wrap_function_wrapper(
135
+ "openai",
136
+ "ChatCompletion.acreate",
137
+ achat_wrapper(
138
+ tracer,
139
+ tokens_histogram,
140
+ chat_choice_counter,
141
+ duration_histogram,
142
+ chat_exception_counter,
143
+ streaming_time_to_first_token,
144
+ streaming_time_to_generate,
145
+ ),
146
+ )
147
+ wrap_function_wrapper(
148
+ "openai",
149
+ "Embedding.create",
150
+ embeddings_wrapper(
151
+ tracer,
152
+ tokens_histogram,
153
+ embeddings_vector_size_counter,
154
+ duration_histogram,
155
+ embeddings_exception_counter,
156
+ ),
157
+ )
158
+ wrap_function_wrapper(
159
+ "openai",
160
+ "Embedding.acreate",
161
+ aembeddings_wrapper(
162
+ tracer,
163
+ tokens_histogram,
164
+ embeddings_vector_size_counter,
165
+ duration_histogram,
166
+ embeddings_exception_counter,
167
+ ),
168
+ )
169
+
170
+ def _uninstrument(self, **kwargs):
171
+ unwrap("openai", "Completion.create")
172
+ unwrap("openai", "Completion.acreate")
173
+ unwrap("openai", "ChatCompletion.create")
174
+ unwrap("openai", "ChatCompletion.acreate")
175
+ unwrap("openai", "Embedding.create")
176
+ unwrap("openai", "Embedding.acreate")