lmnr 0.5.1a0__py3-none-any.whl → 0.5.2__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.
- lmnr/__init__.py +0 -8
- lmnr/openllmetry_sdk/__init__.py +5 -33
- lmnr/openllmetry_sdk/decorators/base.py +24 -17
- lmnr/openllmetry_sdk/instruments.py +1 -0
- lmnr/openllmetry_sdk/opentelemetry/instrumentation/google_genai/__init__.py +454 -0
- lmnr/openllmetry_sdk/opentelemetry/instrumentation/google_genai/config.py +9 -0
- lmnr/openllmetry_sdk/opentelemetry/instrumentation/google_genai/utils.py +216 -0
- lmnr/openllmetry_sdk/tracing/__init__.py +1 -0
- lmnr/openllmetry_sdk/tracing/context_manager.py +13 -0
- lmnr/openllmetry_sdk/tracing/tracing.py +230 -252
- lmnr/sdk/browser/playwright_otel.py +42 -58
- lmnr/sdk/browser/pw_utils.py +8 -40
- lmnr/sdk/client/asynchronous/async_client.py +0 -34
- lmnr/sdk/client/asynchronous/resources/__init__.py +0 -4
- lmnr/sdk/client/asynchronous/resources/agent.py +96 -6
- lmnr/sdk/client/synchronous/resources/__init__.py +1 -3
- lmnr/sdk/client/synchronous/resources/agent.py +94 -8
- lmnr/sdk/client/synchronous/sync_client.py +0 -36
- lmnr/sdk/decorators.py +16 -2
- lmnr/sdk/laminar.py +3 -3
- lmnr/sdk/types.py +84 -170
- lmnr/sdk/utils.py +8 -1
- lmnr/version.py +1 -1
- {lmnr-0.5.1a0.dist-info → lmnr-0.5.2.dist-info}/METADATA +57 -57
- lmnr-0.5.2.dist-info/RECORD +54 -0
- lmnr/sdk/client/asynchronous/resources/pipeline.py +0 -89
- lmnr/sdk/client/asynchronous/resources/semantic_search.py +0 -60
- lmnr/sdk/client/synchronous/resources/pipeline.py +0 -89
- lmnr/sdk/client/synchronous/resources/semantic_search.py +0 -60
- lmnr-0.5.1a0.dist-info/RECORD +0 -54
- {lmnr-0.5.1a0.dist-info → lmnr-0.5.2.dist-info}/LICENSE +0 -0
- {lmnr-0.5.1a0.dist-info → lmnr-0.5.2.dist-info}/WHEEL +0 -0
- {lmnr-0.5.1a0.dist-info → lmnr-0.5.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,216 @@
|
|
1
|
+
import logging
|
2
|
+
import traceback
|
3
|
+
import json
|
4
|
+
|
5
|
+
from .config import (
|
6
|
+
Config,
|
7
|
+
)
|
8
|
+
from google.genai import types
|
9
|
+
from google.genai._common import BaseModel
|
10
|
+
import pydantic
|
11
|
+
from opentelemetry.trace import Span
|
12
|
+
from typing import Any, Optional, Union
|
13
|
+
|
14
|
+
|
15
|
+
def set_span_attribute(span: Span, name: str, value: str):
|
16
|
+
if value is not None:
|
17
|
+
if value != "":
|
18
|
+
span.set_attribute(name, value)
|
19
|
+
return
|
20
|
+
|
21
|
+
|
22
|
+
def dont_throw(func):
|
23
|
+
"""
|
24
|
+
A decorator that wraps the passed in function and logs exceptions instead of throwing them.
|
25
|
+
|
26
|
+
@param func: The function to wrap
|
27
|
+
@return: The wrapper function
|
28
|
+
"""
|
29
|
+
# Obtain a logger specific to the function's module
|
30
|
+
logger = logging.getLogger(func.__module__)
|
31
|
+
|
32
|
+
def wrapper(*args, **kwargs):
|
33
|
+
try:
|
34
|
+
return func(*args, **kwargs)
|
35
|
+
except Exception as e:
|
36
|
+
logger.debug(
|
37
|
+
"OpenLLMetry failed to trace in %s, error: %s",
|
38
|
+
func.__name__,
|
39
|
+
traceback.format_exc(),
|
40
|
+
)
|
41
|
+
if Config.exception_logger:
|
42
|
+
Config.exception_logger(e)
|
43
|
+
|
44
|
+
return wrapper
|
45
|
+
|
46
|
+
|
47
|
+
def to_dict(obj: Union[BaseModel, pydantic.BaseModel, dict]) -> dict[str, Any]:
|
48
|
+
try:
|
49
|
+
if isinstance(obj, BaseModel):
|
50
|
+
return obj.model_dump()
|
51
|
+
elif isinstance(obj, pydantic.BaseModel):
|
52
|
+
return obj.model_dump()
|
53
|
+
elif isinstance(obj, dict):
|
54
|
+
return obj
|
55
|
+
else:
|
56
|
+
return dict(obj)
|
57
|
+
except Exception:
|
58
|
+
return dict(obj)
|
59
|
+
|
60
|
+
|
61
|
+
def process_content_union(
|
62
|
+
content: Union[types.ContentUnion, types.ContentUnionDict],
|
63
|
+
trace_id: Optional[str] = None,
|
64
|
+
span_id: Optional[str] = None,
|
65
|
+
message_index: int = 0,
|
66
|
+
) -> Optional[str]:
|
67
|
+
parts = _process_content_union(content, trace_id, span_id, message_index)
|
68
|
+
if parts is None:
|
69
|
+
return None
|
70
|
+
if isinstance(parts, str):
|
71
|
+
return parts
|
72
|
+
elif isinstance(parts, list):
|
73
|
+
if len(parts) == 1 and isinstance(parts[0], str):
|
74
|
+
return parts[0]
|
75
|
+
return json.dumps(
|
76
|
+
[
|
77
|
+
{"type": "text", "text": part} if isinstance(part, str) else part
|
78
|
+
for part in parts
|
79
|
+
]
|
80
|
+
)
|
81
|
+
else:
|
82
|
+
return None
|
83
|
+
|
84
|
+
|
85
|
+
def _process_content_union(
|
86
|
+
content: Union[types.ContentUnion, types.ContentUnionDict],
|
87
|
+
trace_id: Optional[str] = None,
|
88
|
+
span_id: Optional[str] = None,
|
89
|
+
message_index: int = 0,
|
90
|
+
) -> Union[str, list[str], None]:
|
91
|
+
if isinstance(content, types.Content):
|
92
|
+
parts = to_dict(content).get("parts", [])
|
93
|
+
return [_process_part(part) for part in parts]
|
94
|
+
elif isinstance(content, list):
|
95
|
+
return [_process_part_union(item) for item in content]
|
96
|
+
elif isinstance(content, (types.Part, types.File, str)):
|
97
|
+
return _process_part_union(content)
|
98
|
+
elif isinstance(content, dict):
|
99
|
+
if "parts" in content:
|
100
|
+
return [
|
101
|
+
_process_part_union(
|
102
|
+
item, trace_id, span_id, message_index, content_index
|
103
|
+
)
|
104
|
+
for content_index, item in enumerate(content.get("parts", []))
|
105
|
+
]
|
106
|
+
else:
|
107
|
+
# Assume it's PartDict
|
108
|
+
return _process_part_union(content, trace_id, span_id, message_index)
|
109
|
+
else:
|
110
|
+
return None
|
111
|
+
|
112
|
+
|
113
|
+
def _process_part_union(
|
114
|
+
content: Union[types.PartDict, types.File, types.Part, str],
|
115
|
+
trace_id: Optional[str] = None,
|
116
|
+
span_id: Optional[str] = None,
|
117
|
+
message_index: int = 0,
|
118
|
+
content_index: int = 0,
|
119
|
+
) -> Optional[str]:
|
120
|
+
if isinstance(content, str):
|
121
|
+
return content
|
122
|
+
elif isinstance(content, types.File):
|
123
|
+
content_dict = to_dict(content)
|
124
|
+
name = (
|
125
|
+
content_dict.get("name")
|
126
|
+
or content_dict.get("display_name")
|
127
|
+
or content_dict.get("uri")
|
128
|
+
)
|
129
|
+
return f"files/{name}"
|
130
|
+
elif isinstance(content, (types.Part, dict)):
|
131
|
+
return _process_part(content, trace_id, span_id, message_index, content_index)
|
132
|
+
else:
|
133
|
+
return None
|
134
|
+
|
135
|
+
|
136
|
+
def _process_part(
|
137
|
+
content: types.Part,
|
138
|
+
trace_id: Optional[str] = None,
|
139
|
+
span_id: Optional[str] = None,
|
140
|
+
message_index: int = 0,
|
141
|
+
content_index: int = 0,
|
142
|
+
) -> Optional[str]:
|
143
|
+
part_dict = to_dict(content)
|
144
|
+
if part_dict.get("text") is not None:
|
145
|
+
return part_dict.get("text")
|
146
|
+
elif part_dict.get("inline_data"):
|
147
|
+
blob = to_dict(part_dict.get("inline_data"))
|
148
|
+
if blob.get("mime_type").startswith("image/"):
|
149
|
+
return _process_image_item(
|
150
|
+
blob, trace_id, span_id, message_index, content_index
|
151
|
+
)
|
152
|
+
else:
|
153
|
+
# currently, only images are supported
|
154
|
+
return blob.get("mime_type") or "unknown_media"
|
155
|
+
else:
|
156
|
+
return None
|
157
|
+
|
158
|
+
|
159
|
+
def role_from_content_union(
|
160
|
+
content: Union[types.ContentUnion, types.ContentUnionDict],
|
161
|
+
) -> Optional[str]:
|
162
|
+
if isinstance(content, types.Content):
|
163
|
+
return to_dict(content).get("role")
|
164
|
+
elif isinstance(content, list) and len(content) > 0:
|
165
|
+
return role_from_content_union(content[0])
|
166
|
+
else:
|
167
|
+
return None
|
168
|
+
|
169
|
+
|
170
|
+
def with_tracer_wrapper(func):
|
171
|
+
"""Helper for providing tracer for wrapper functions."""
|
172
|
+
|
173
|
+
def _with_tracer(tracer, to_wrap):
|
174
|
+
def wrapper(wrapped, instance, args, kwargs):
|
175
|
+
return func(tracer, to_wrap, wrapped, instance, args, kwargs)
|
176
|
+
|
177
|
+
return wrapper
|
178
|
+
|
179
|
+
return _with_tracer
|
180
|
+
|
181
|
+
|
182
|
+
def _run_async(method):
|
183
|
+
import asyncio
|
184
|
+
import threading
|
185
|
+
|
186
|
+
try:
|
187
|
+
loop = asyncio.get_running_loop()
|
188
|
+
except RuntimeError:
|
189
|
+
loop = None
|
190
|
+
|
191
|
+
if loop and loop.is_running():
|
192
|
+
thread = threading.Thread(target=lambda: asyncio.run(method))
|
193
|
+
thread.start()
|
194
|
+
thread.join()
|
195
|
+
else:
|
196
|
+
asyncio.run(method)
|
197
|
+
|
198
|
+
|
199
|
+
def _process_image_item(
|
200
|
+
blob: dict[str, Any],
|
201
|
+
trace_id: str,
|
202
|
+
span_id: str,
|
203
|
+
message_index: int,
|
204
|
+
content_index: int,
|
205
|
+
):
|
206
|
+
# Convert to openai format, so backends can handle it
|
207
|
+
return (
|
208
|
+
{
|
209
|
+
"type": "image_url",
|
210
|
+
"image_url": {
|
211
|
+
"url": f"data:image/{blob.get('mime_type').split('/')[1]};base64,{blob.get('data')}",
|
212
|
+
},
|
213
|
+
}
|
214
|
+
if Config.convert_image_to_openai_format
|
215
|
+
else blob
|
216
|
+
)
|
@@ -0,0 +1 @@
|
|
1
|
+
from lmnr.openllmetry_sdk.tracing.context_manager import get_tracer
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from contextlib import contextmanager
|
2
|
+
|
3
|
+
from lmnr.openllmetry_sdk.tracing.tracing import TracerWrapper
|
4
|
+
|
5
|
+
|
6
|
+
@contextmanager
|
7
|
+
def get_tracer(flush_on_exit: bool = False):
|
8
|
+
wrapper = TracerWrapper()
|
9
|
+
try:
|
10
|
+
yield wrapper.get_tracer()
|
11
|
+
finally:
|
12
|
+
if flush_on_exit:
|
13
|
+
wrapper.flush()
|