lucidicai 1.2.15__py3-none-any.whl → 1.2.17__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.
- lucidicai/__init__.py +111 -21
- lucidicai/client.py +22 -5
- lucidicai/decorators.py +357 -0
- lucidicai/event.py +2 -2
- lucidicai/image_upload.py +24 -1
- lucidicai/providers/anthropic_handler.py +0 -7
- lucidicai/providers/image_storage.py +45 -0
- lucidicai/providers/langchain.py +0 -78
- lucidicai/providers/lucidic_exporter.py +259 -0
- lucidicai/providers/lucidic_span_processor.py +648 -0
- lucidicai/providers/openai_agents_instrumentor.py +307 -0
- lucidicai/providers/openai_handler.py +1 -56
- lucidicai/providers/otel_handlers.py +266 -0
- lucidicai/providers/otel_init.py +197 -0
- lucidicai/providers/otel_provider.py +168 -0
- lucidicai/providers/pydantic_ai_handler.py +2 -19
- lucidicai/providers/text_storage.py +53 -0
- lucidicai/providers/universal_image_interceptor.py +276 -0
- lucidicai/session.py +17 -4
- lucidicai/step.py +4 -4
- lucidicai/streaming.py +2 -3
- lucidicai/telemetry/__init__.py +0 -0
- lucidicai/telemetry/base_provider.py +21 -0
- lucidicai/telemetry/lucidic_exporter.py +259 -0
- lucidicai/telemetry/lucidic_span_processor.py +665 -0
- lucidicai/telemetry/openai_agents_instrumentor.py +306 -0
- lucidicai/telemetry/opentelemetry_converter.py +436 -0
- lucidicai/telemetry/otel_handlers.py +266 -0
- lucidicai/telemetry/otel_init.py +197 -0
- lucidicai/telemetry/otel_provider.py +168 -0
- lucidicai/telemetry/pydantic_ai_handler.py +600 -0
- lucidicai/telemetry/utils/__init__.py +0 -0
- lucidicai/telemetry/utils/image_storage.py +45 -0
- lucidicai/telemetry/utils/text_storage.py +53 -0
- lucidicai/telemetry/utils/universal_image_interceptor.py +276 -0
- {lucidicai-1.2.15.dist-info → lucidicai-1.2.17.dist-info}/METADATA +1 -1
- lucidicai-1.2.17.dist-info/RECORD +49 -0
- lucidicai-1.2.15.dist-info/RECORD +0 -25
- {lucidicai-1.2.15.dist-info → lucidicai-1.2.17.dist-info}/WHEEL +0 -0
- {lucidicai-1.2.15.dist-info → lucidicai-1.2.17.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""OpenTelemetry-based handlers that maintain backward compatibility"""
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .base_provider import BaseProvider
|
|
6
|
+
from .otel_init import LucidicTelemetry
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("Lucidic")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OTelOpenAIHandler(BaseProvider):
|
|
12
|
+
"""OpenAI handler using OpenTelemetry instrumentation"""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
super().__init__()
|
|
16
|
+
self._provider_name = "OpenAI"
|
|
17
|
+
self.telemetry = LucidicTelemetry()
|
|
18
|
+
|
|
19
|
+
def handle_response(self, response, kwargs, session: Optional = None):
|
|
20
|
+
"""Not needed with OpenTelemetry approach"""
|
|
21
|
+
return response
|
|
22
|
+
|
|
23
|
+
def override(self):
|
|
24
|
+
"""Enable OpenAI instrumentation"""
|
|
25
|
+
try:
|
|
26
|
+
from lucidicai.client import Client
|
|
27
|
+
client = Client()
|
|
28
|
+
|
|
29
|
+
# Initialize telemetry if needed
|
|
30
|
+
if not self.telemetry.is_initialized():
|
|
31
|
+
self.telemetry.initialize(agent_id=client.agent_id)
|
|
32
|
+
|
|
33
|
+
# Instrument OpenAI
|
|
34
|
+
self.telemetry.instrument_providers(["openai"])
|
|
35
|
+
|
|
36
|
+
# Also patch OpenAI client to intercept images
|
|
37
|
+
try:
|
|
38
|
+
import openai
|
|
39
|
+
from .utils.universal_image_interceptor import UniversalImageInterceptor, patch_openai_client
|
|
40
|
+
|
|
41
|
+
# Create interceptor for OpenAI
|
|
42
|
+
interceptor = UniversalImageInterceptor.create_interceptor("openai")
|
|
43
|
+
|
|
44
|
+
# Patch the module-level create method
|
|
45
|
+
if hasattr(openai, 'ChatCompletion'):
|
|
46
|
+
# Old API
|
|
47
|
+
original = openai.ChatCompletion.create
|
|
48
|
+
openai.ChatCompletion.create = interceptor(original)
|
|
49
|
+
|
|
50
|
+
# Also patch any client instances that might be created
|
|
51
|
+
original_client_init = openai.OpenAI.__init__
|
|
52
|
+
def patched_init(self, *args, **kwargs):
|
|
53
|
+
original_client_init(self, *args, **kwargs)
|
|
54
|
+
# Patch this instance
|
|
55
|
+
patch_openai_client(self)
|
|
56
|
+
|
|
57
|
+
openai.OpenAI.__init__ = patched_init
|
|
58
|
+
|
|
59
|
+
# Also patch AsyncOpenAI
|
|
60
|
+
if hasattr(openai, 'AsyncOpenAI'):
|
|
61
|
+
original_async_init = openai.AsyncOpenAI.__init__
|
|
62
|
+
def patched_async_init(self, *args, **kwargs):
|
|
63
|
+
original_async_init(self, *args, **kwargs)
|
|
64
|
+
# Patch this instance
|
|
65
|
+
patch_openai_client(self)
|
|
66
|
+
|
|
67
|
+
openai.AsyncOpenAI.__init__ = patched_async_init
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.warning(f"Could not patch OpenAI for image interception: {e}")
|
|
71
|
+
|
|
72
|
+
logger.info("[OTel OpenAI Handler] Instrumentation enabled")
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error(f"Failed to enable OpenAI instrumentation: {e}")
|
|
76
|
+
raise
|
|
77
|
+
|
|
78
|
+
def undo_override(self):
|
|
79
|
+
"""Disable instrumentation"""
|
|
80
|
+
# Telemetry uninstrumentation is handled globally
|
|
81
|
+
logger.info("[OTel OpenAI Handler] Instrumentation will be disabled on shutdown")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class OTelAnthropicHandler(BaseProvider):
|
|
85
|
+
"""Anthropic handler using OpenTelemetry instrumentation"""
|
|
86
|
+
|
|
87
|
+
def __init__(self):
|
|
88
|
+
super().__init__()
|
|
89
|
+
self._provider_name = "Anthropic"
|
|
90
|
+
self.telemetry = LucidicTelemetry()
|
|
91
|
+
|
|
92
|
+
def handle_response(self, response, kwargs, session: Optional = None):
|
|
93
|
+
"""Not needed with OpenTelemetry approach"""
|
|
94
|
+
return response
|
|
95
|
+
|
|
96
|
+
def override(self):
|
|
97
|
+
"""Enable Anthropic instrumentation"""
|
|
98
|
+
try:
|
|
99
|
+
from lucidicai.client import Client
|
|
100
|
+
client = Client()
|
|
101
|
+
|
|
102
|
+
# Initialize telemetry if needed
|
|
103
|
+
if not self.telemetry.is_initialized():
|
|
104
|
+
self.telemetry.initialize(agent_id=client.agent_id)
|
|
105
|
+
|
|
106
|
+
# Instrument Anthropic
|
|
107
|
+
self.telemetry.instrument_providers(["anthropic"])
|
|
108
|
+
|
|
109
|
+
# Also patch Anthropic client to intercept images
|
|
110
|
+
try:
|
|
111
|
+
import anthropic
|
|
112
|
+
from .utils.universal_image_interceptor import UniversalImageInterceptor, patch_anthropic_client
|
|
113
|
+
|
|
114
|
+
# Create interceptors for Anthropic
|
|
115
|
+
interceptor = UniversalImageInterceptor.create_interceptor("anthropic")
|
|
116
|
+
async_interceptor = UniversalImageInterceptor.create_async_interceptor("anthropic")
|
|
117
|
+
|
|
118
|
+
# Patch any client instances that might be created
|
|
119
|
+
original_client_init = anthropic.Anthropic.__init__
|
|
120
|
+
def patched_init(self, *args, **kwargs):
|
|
121
|
+
original_client_init(self, *args, **kwargs)
|
|
122
|
+
# Patch this instance
|
|
123
|
+
patch_anthropic_client(self)
|
|
124
|
+
|
|
125
|
+
anthropic.Anthropic.__init__ = patched_init
|
|
126
|
+
|
|
127
|
+
# Also patch async client
|
|
128
|
+
if hasattr(anthropic, 'AsyncAnthropic'):
|
|
129
|
+
original_async_init = anthropic.AsyncAnthropic.__init__
|
|
130
|
+
def patched_async_init(self, *args, **kwargs):
|
|
131
|
+
original_async_init(self, *args, **kwargs)
|
|
132
|
+
# Patch this instance
|
|
133
|
+
patch_anthropic_client(self)
|
|
134
|
+
|
|
135
|
+
anthropic.AsyncAnthropic.__init__ = patched_async_init
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.warning(f"Could not patch Anthropic for image interception: {e}")
|
|
139
|
+
|
|
140
|
+
logger.info("[OTel Anthropic Handler] Instrumentation enabled")
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.error(f"Failed to enable Anthropic instrumentation: {e}")
|
|
144
|
+
raise
|
|
145
|
+
|
|
146
|
+
def undo_override(self):
|
|
147
|
+
"""Disable instrumentation"""
|
|
148
|
+
logger.info("[OTel Anthropic Handler] Instrumentation will be disabled on shutdown")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class OTelLangChainHandler(BaseProvider):
|
|
152
|
+
"""LangChain handler using OpenTelemetry instrumentation"""
|
|
153
|
+
|
|
154
|
+
def __init__(self):
|
|
155
|
+
super().__init__()
|
|
156
|
+
self._provider_name = "LangChain"
|
|
157
|
+
self.telemetry = LucidicTelemetry()
|
|
158
|
+
|
|
159
|
+
def handle_response(self, response, kwargs, session: Optional = None):
|
|
160
|
+
"""Not needed with OpenTelemetry approach"""
|
|
161
|
+
return response
|
|
162
|
+
|
|
163
|
+
def override(self):
|
|
164
|
+
"""Enable LangChain instrumentation"""
|
|
165
|
+
try:
|
|
166
|
+
from lucidicai.client import Client
|
|
167
|
+
client = Client()
|
|
168
|
+
|
|
169
|
+
# Initialize telemetry if needed
|
|
170
|
+
if not self.telemetry.is_initialized():
|
|
171
|
+
self.telemetry.initialize(agent_id=client.agent_id)
|
|
172
|
+
|
|
173
|
+
# Instrument LangChain
|
|
174
|
+
self.telemetry.instrument_providers(["langchain"])
|
|
175
|
+
|
|
176
|
+
logger.info("[OTel LangChain Handler] Instrumentation enabled")
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(f"Failed to enable LangChain instrumentation: {e}")
|
|
180
|
+
raise
|
|
181
|
+
|
|
182
|
+
def undo_override(self):
|
|
183
|
+
"""Disable instrumentation"""
|
|
184
|
+
logger.info("[OTel LangChain Handler] Instrumentation will be disabled on shutdown")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class OTelPydanticAIHandler(BaseProvider):
|
|
188
|
+
"""Pydantic AI handler - requires custom implementation"""
|
|
189
|
+
|
|
190
|
+
def __init__(self):
|
|
191
|
+
super().__init__()
|
|
192
|
+
self._provider_name = "PydanticAI"
|
|
193
|
+
self.telemetry = LucidicTelemetry()
|
|
194
|
+
self._original_methods = {}
|
|
195
|
+
|
|
196
|
+
def handle_response(self, response, kwargs, session: Optional = None):
|
|
197
|
+
"""Handle Pydantic AI responses"""
|
|
198
|
+
return response
|
|
199
|
+
|
|
200
|
+
def override(self):
|
|
201
|
+
"""Enable Pydantic AI instrumentation"""
|
|
202
|
+
try:
|
|
203
|
+
from lucidicai.client import Client
|
|
204
|
+
client = Client()
|
|
205
|
+
|
|
206
|
+
# Initialize telemetry if needed
|
|
207
|
+
if not self.telemetry.is_initialized():
|
|
208
|
+
self.telemetry.initialize(agent_id=client.agent_id)
|
|
209
|
+
|
|
210
|
+
# For now, we'll use the original Pydantic AI handler
|
|
211
|
+
# until OpenLLMetry adds support
|
|
212
|
+
from .pydantic_ai_handler import PydanticAIHandler
|
|
213
|
+
self._fallback_handler = PydanticAIHandler()
|
|
214
|
+
self._fallback_handler.override()
|
|
215
|
+
|
|
216
|
+
logger.info("[OTel PydanticAI Handler] Using fallback handler until OpenLLMetry support is available")
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.error(f"Failed to enable Pydantic AI instrumentation: {e}")
|
|
220
|
+
raise
|
|
221
|
+
|
|
222
|
+
def undo_override(self):
|
|
223
|
+
"""Disable instrumentation"""
|
|
224
|
+
if hasattr(self, '_fallback_handler'):
|
|
225
|
+
self._fallback_handler.undo_override()
|
|
226
|
+
logger.info("[OTel PydanticAI Handler] Instrumentation disabled")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class OTelOpenAIAgentsHandler(BaseProvider):
|
|
230
|
+
"""OpenAI Agents handler using OpenTelemetry instrumentation"""
|
|
231
|
+
|
|
232
|
+
def __init__(self):
|
|
233
|
+
super().__init__()
|
|
234
|
+
self._provider_name = "OpenAI Agents"
|
|
235
|
+
self.telemetry = LucidicTelemetry()
|
|
236
|
+
self._is_instrumented = False
|
|
237
|
+
|
|
238
|
+
def handle_response(self, response, kwargs, session: Optional = None):
|
|
239
|
+
"""Not needed with OpenTelemetry approach"""
|
|
240
|
+
return response
|
|
241
|
+
|
|
242
|
+
def override(self):
|
|
243
|
+
"""Enable OpenAI Agents instrumentation"""
|
|
244
|
+
try:
|
|
245
|
+
from lucidicai.client import Client
|
|
246
|
+
client = Client()
|
|
247
|
+
|
|
248
|
+
# Initialize telemetry if needed
|
|
249
|
+
if not self.telemetry.is_initialized():
|
|
250
|
+
self.telemetry.initialize(agent_id=client.agent_id)
|
|
251
|
+
|
|
252
|
+
# Only instrument OpenAI Agents (it will handle OpenAI calls internally)
|
|
253
|
+
self.telemetry.instrument_providers(["openai_agents"])
|
|
254
|
+
|
|
255
|
+
self._is_instrumented = True
|
|
256
|
+
|
|
257
|
+
logger.info("[OTel OpenAI Agents Handler] Full instrumentation enabled")
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
logger.error(f"Failed to enable OpenAI Agents instrumentation: {e}")
|
|
261
|
+
raise
|
|
262
|
+
|
|
263
|
+
def undo_override(self):
|
|
264
|
+
"""Disable instrumentation"""
|
|
265
|
+
self._is_instrumented = False
|
|
266
|
+
logger.info("[OTel OpenAI Agents Handler] Instrumentation will be disabled on shutdown")
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""OpenTelemetry initialization and configuration for Lucidic"""
|
|
2
|
+
import logging
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from opentelemetry import trace
|
|
6
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
7
|
+
from opentelemetry.sdk.resources import Resource
|
|
8
|
+
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
|
|
9
|
+
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
|
|
10
|
+
from opentelemetry.instrumentation.langchain import LangchainInstrumentor
|
|
11
|
+
|
|
12
|
+
from .lucidic_span_processor import LucidicSpanProcessor
|
|
13
|
+
from .otel_provider import OpenTelemetryProvider
|
|
14
|
+
from lucidicai.client import Client
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("Lucidic")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LucidicTelemetry:
|
|
20
|
+
"""Manages OpenTelemetry initialization for Lucidic"""
|
|
21
|
+
|
|
22
|
+
_instance = None
|
|
23
|
+
_initialized = False
|
|
24
|
+
|
|
25
|
+
def __new__(cls):
|
|
26
|
+
if cls._instance is None:
|
|
27
|
+
cls._instance = super().__new__(cls)
|
|
28
|
+
return cls._instance
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
if not self._initialized:
|
|
32
|
+
self.tracer_provider = None
|
|
33
|
+
self.span_processor = None
|
|
34
|
+
self.instrumentors = {}
|
|
35
|
+
self.provider = OpenTelemetryProvider()
|
|
36
|
+
self._initialized = True
|
|
37
|
+
|
|
38
|
+
def initialize(self, agent_id: str, service_name: str = "lucidic-ai") -> None:
|
|
39
|
+
"""Initialize OpenTelemetry with Lucidic configuration"""
|
|
40
|
+
if self.tracer_provider:
|
|
41
|
+
logger.debug("OpenTelemetry already initialized")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# Create resource
|
|
46
|
+
resource = Resource.create({
|
|
47
|
+
"service.name": service_name,
|
|
48
|
+
"service.version": "1.0.0",
|
|
49
|
+
"lucidic.agent_id": agent_id,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
# Create tracer provider
|
|
53
|
+
self.tracer_provider = TracerProvider(resource=resource)
|
|
54
|
+
|
|
55
|
+
# Add our custom span processor for real-time event handling
|
|
56
|
+
self.span_processor = LucidicSpanProcessor()
|
|
57
|
+
self.tracer_provider.add_span_processor(self.span_processor)
|
|
58
|
+
|
|
59
|
+
# Set as global provider
|
|
60
|
+
trace.set_tracer_provider(self.tracer_provider)
|
|
61
|
+
|
|
62
|
+
logger.info("[LucidicTelemetry] OpenTelemetry initialized")
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"Failed to initialize OpenTelemetry: {e}")
|
|
66
|
+
raise
|
|
67
|
+
|
|
68
|
+
def instrument_providers(self, providers: List[str]) -> None:
|
|
69
|
+
"""Instrument specified providers"""
|
|
70
|
+
for provider in providers:
|
|
71
|
+
try:
|
|
72
|
+
if provider == "openai" and provider not in self.instrumentors:
|
|
73
|
+
self._instrument_openai()
|
|
74
|
+
elif provider == "anthropic" and provider not in self.instrumentors:
|
|
75
|
+
self._instrument_anthropic()
|
|
76
|
+
elif provider == "langchain" and provider not in self.instrumentors:
|
|
77
|
+
self._instrument_langchain()
|
|
78
|
+
elif provider == "pydantic_ai":
|
|
79
|
+
# Custom instrumentation needed
|
|
80
|
+
logger.info(f"[LucidicTelemetry] Pydantic AI will use manual instrumentation")
|
|
81
|
+
elif provider == "openai_agents":
|
|
82
|
+
# OpenAI Agents uses the same OpenAI instrumentation
|
|
83
|
+
self._instrument_openai_agents()
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.error(f"Failed to instrument {provider}: {e}")
|
|
86
|
+
|
|
87
|
+
def _instrument_openai(self) -> None:
|
|
88
|
+
"""Instrument OpenAI"""
|
|
89
|
+
try:
|
|
90
|
+
# Get client for masking function
|
|
91
|
+
client = Client()
|
|
92
|
+
|
|
93
|
+
# Configure instrumentation
|
|
94
|
+
instrumentor = OpenAIInstrumentor()
|
|
95
|
+
|
|
96
|
+
# Create a custom callback for getting attributes
|
|
97
|
+
def get_custom_attributes():
|
|
98
|
+
attrs = {}
|
|
99
|
+
|
|
100
|
+
# Add step context if available
|
|
101
|
+
if client.session and client.session.active_step:
|
|
102
|
+
attrs["lucidic.step_id"] = client.session.active_step.step_id
|
|
103
|
+
|
|
104
|
+
return attrs
|
|
105
|
+
|
|
106
|
+
instrumentor.instrument(
|
|
107
|
+
tracer_provider=self.tracer_provider,
|
|
108
|
+
enrich_token_usage=True,
|
|
109
|
+
exception_logger=lambda e: logger.error(f"OpenAI error: {e}"),
|
|
110
|
+
get_common_metrics_attributes=get_custom_attributes,
|
|
111
|
+
enable_trace_context_propagation=True,
|
|
112
|
+
use_legacy_attributes=True # Force legacy attributes mode for now
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
self.instrumentors["openai"] = instrumentor
|
|
116
|
+
logger.info("[LucidicTelemetry] Instrumented OpenAI")
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(f"Failed to instrument OpenAI: {e}")
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
def _instrument_anthropic(self) -> None:
|
|
123
|
+
"""Instrument Anthropic"""
|
|
124
|
+
try:
|
|
125
|
+
instrumentor = AnthropicInstrumentor()
|
|
126
|
+
|
|
127
|
+
# Get client for context
|
|
128
|
+
client = Client()
|
|
129
|
+
|
|
130
|
+
def get_custom_attributes():
|
|
131
|
+
attrs = {}
|
|
132
|
+
if client.session and client.session.active_step:
|
|
133
|
+
attrs["lucidic.step_id"] = client.session.active_step.step_id
|
|
134
|
+
return attrs
|
|
135
|
+
|
|
136
|
+
instrumentor.instrument(
|
|
137
|
+
tracer_provider=self.tracer_provider,
|
|
138
|
+
exception_logger=lambda e: logger.error(f"Anthropic error: {e}"),
|
|
139
|
+
get_common_metrics_attributes=get_custom_attributes,
|
|
140
|
+
use_legacy_attributes=True # Force legacy attributes mode
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
self.instrumentors["anthropic"] = instrumentor
|
|
144
|
+
logger.info("[LucidicTelemetry] Instrumented Anthropic")
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Failed to instrument Anthropic: {e}")
|
|
148
|
+
raise
|
|
149
|
+
|
|
150
|
+
def _instrument_langchain(self) -> None:
|
|
151
|
+
"""Instrument LangChain"""
|
|
152
|
+
try:
|
|
153
|
+
instrumentor = LangchainInstrumentor()
|
|
154
|
+
instrumentor.instrument(tracer_provider=self.tracer_provider)
|
|
155
|
+
|
|
156
|
+
self.instrumentors["langchain"] = instrumentor
|
|
157
|
+
logger.info("[LucidicTelemetry] Instrumented LangChain")
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"Failed to instrument LangChain: {e}")
|
|
161
|
+
raise
|
|
162
|
+
|
|
163
|
+
def _instrument_openai_agents(self) -> None:
|
|
164
|
+
"""Instrument OpenAI Agents SDK"""
|
|
165
|
+
try:
|
|
166
|
+
from .openai_agents_instrumentor import OpenAIAgentsInstrumentor
|
|
167
|
+
|
|
168
|
+
instrumentor = OpenAIAgentsInstrumentor(tracer_provider=self.tracer_provider)
|
|
169
|
+
instrumentor.instrument()
|
|
170
|
+
|
|
171
|
+
self.instrumentors["openai_agents"] = instrumentor
|
|
172
|
+
logger.info("[LucidicTelemetry] Instrumented OpenAI Agents SDK")
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error(f"Failed to instrument OpenAI Agents SDK: {e}")
|
|
176
|
+
raise
|
|
177
|
+
|
|
178
|
+
def uninstrument_all(self) -> None:
|
|
179
|
+
"""Uninstrument all providers"""
|
|
180
|
+
for name, instrumentor in self.instrumentors.items():
|
|
181
|
+
try:
|
|
182
|
+
instrumentor.uninstrument()
|
|
183
|
+
logger.info(f"[LucidicTelemetry] Uninstrumented {name}")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.error(f"Failed to uninstrument {name}: {e}")
|
|
186
|
+
|
|
187
|
+
self.instrumentors.clear()
|
|
188
|
+
|
|
189
|
+
# Shutdown tracer provider
|
|
190
|
+
if self.tracer_provider:
|
|
191
|
+
self.tracer_provider.shutdown()
|
|
192
|
+
self.tracer_provider = None
|
|
193
|
+
self.span_processor = None
|
|
194
|
+
|
|
195
|
+
def is_initialized(self) -> bool:
|
|
196
|
+
"""Check if telemetry is initialized"""
|
|
197
|
+
return self.tracer_provider is not None
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""OpenTelemetry-based provider implementation"""
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional, List, Dict, Any
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
|
|
6
|
+
from opentelemetry import trace, context
|
|
7
|
+
from opentelemetry.trace import Tracer, Span
|
|
8
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
9
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
10
|
+
from opentelemetry.sdk.resources import Resource
|
|
11
|
+
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
|
|
12
|
+
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
|
|
13
|
+
from opentelemetry.instrumentation.langchain import LangchainInstrumentor
|
|
14
|
+
from opentelemetry.semconv_ai import SpanAttributes
|
|
15
|
+
|
|
16
|
+
from .lucidic_exporter import LucidicSpanExporter
|
|
17
|
+
from .base_provider import BaseProvider
|
|
18
|
+
from lucidicai.client import Client
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("Lucidic")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OpenTelemetryProvider(BaseProvider):
|
|
24
|
+
"""Provider that uses OpenTelemetry instrumentations instead of monkey-patching"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
super().__init__()
|
|
28
|
+
self._provider_name = "OpenTelemetry"
|
|
29
|
+
self.tracer_provider = None
|
|
30
|
+
self.tracer = None
|
|
31
|
+
self.instrumentors = {}
|
|
32
|
+
self._active_spans = {}
|
|
33
|
+
|
|
34
|
+
def initialize_telemetry(self, service_name: str = "lucidic-ai", agent_id: str = None) -> None:
|
|
35
|
+
"""Initialize OpenTelemetry with Lucidic exporter"""
|
|
36
|
+
# Create resource with service info
|
|
37
|
+
resource = Resource.create({
|
|
38
|
+
"service.name": service_name,
|
|
39
|
+
"service.version": "1.0.0",
|
|
40
|
+
"lucidic.agent_id": agent_id or ""
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
# Create tracer provider
|
|
44
|
+
self.tracer_provider = TracerProvider(resource=resource)
|
|
45
|
+
|
|
46
|
+
# Add our custom exporter
|
|
47
|
+
lucidic_exporter = LucidicSpanExporter()
|
|
48
|
+
span_processor = BatchSpanProcessor(lucidic_exporter)
|
|
49
|
+
self.tracer_provider.add_span_processor(span_processor)
|
|
50
|
+
|
|
51
|
+
# Set as global provider
|
|
52
|
+
trace.set_tracer_provider(self.tracer_provider)
|
|
53
|
+
|
|
54
|
+
# Get tracer
|
|
55
|
+
self.tracer = trace.get_tracer(__name__)
|
|
56
|
+
|
|
57
|
+
def handle_response(self, response, kwargs, session: Optional = None):
|
|
58
|
+
"""Handle responses - not needed with OTEL approach"""
|
|
59
|
+
return response
|
|
60
|
+
|
|
61
|
+
def override(self):
|
|
62
|
+
"""Initialize OpenTelemetry instrumentations"""
|
|
63
|
+
try:
|
|
64
|
+
client = Client()
|
|
65
|
+
|
|
66
|
+
# Initialize telemetry if not already done
|
|
67
|
+
if not self.tracer_provider:
|
|
68
|
+
self.initialize_telemetry(agent_id=client.agent_id)
|
|
69
|
+
|
|
70
|
+
# No actual override needed - instrumentations will be enabled separately
|
|
71
|
+
logger.info("[OpenTelemetry Provider] Initialized")
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(f"Failed to initialize OpenTelemetry: {e}")
|
|
75
|
+
raise
|
|
76
|
+
|
|
77
|
+
def undo_override(self):
|
|
78
|
+
"""Uninstrument all providers"""
|
|
79
|
+
for name, instrumentor in self.instrumentors.items():
|
|
80
|
+
try:
|
|
81
|
+
instrumentor.uninstrument()
|
|
82
|
+
logger.info(f"[OpenTelemetry Provider] Uninstrumented {name}")
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(f"Failed to uninstrument {name}: {e}")
|
|
85
|
+
|
|
86
|
+
self.instrumentors.clear()
|
|
87
|
+
|
|
88
|
+
# Shutdown tracer provider
|
|
89
|
+
if self.tracer_provider:
|
|
90
|
+
self.tracer_provider.shutdown()
|
|
91
|
+
|
|
92
|
+
def instrument_openai(self) -> None:
|
|
93
|
+
"""Instrument OpenAI with OpenLLMetry"""
|
|
94
|
+
if "openai" not in self.instrumentors:
|
|
95
|
+
try:
|
|
96
|
+
instrumentor = OpenAIInstrumentor()
|
|
97
|
+
instrumentor.instrument(
|
|
98
|
+
tracer_provider=self.tracer_provider,
|
|
99
|
+
enrich_token_usage=True,
|
|
100
|
+
exception_logger=lambda e: logger.error(f"OpenAI error: {e}")
|
|
101
|
+
)
|
|
102
|
+
self.instrumentors["openai"] = instrumentor
|
|
103
|
+
logger.info("[OpenTelemetry Provider] Instrumented OpenAI")
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.error(f"Failed to instrument OpenAI: {e}")
|
|
106
|
+
|
|
107
|
+
def instrument_anthropic(self) -> None:
|
|
108
|
+
"""Instrument Anthropic with OpenLLMetry"""
|
|
109
|
+
if "anthropic" not in self.instrumentors:
|
|
110
|
+
try:
|
|
111
|
+
instrumentor = AnthropicInstrumentor()
|
|
112
|
+
instrumentor.instrument(
|
|
113
|
+
tracer_provider=self.tracer_provider,
|
|
114
|
+
exception_logger=lambda e: logger.error(f"Anthropic error: {e}")
|
|
115
|
+
)
|
|
116
|
+
self.instrumentors["anthropic"] = instrumentor
|
|
117
|
+
logger.info("[OpenTelemetry Provider] Instrumented Anthropic")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(f"Failed to instrument Anthropic: {e}")
|
|
120
|
+
|
|
121
|
+
def instrument_langchain(self) -> None:
|
|
122
|
+
"""Instrument LangChain with OpenLLMetry"""
|
|
123
|
+
if "langchain" not in self.instrumentors:
|
|
124
|
+
try:
|
|
125
|
+
instrumentor = LangchainInstrumentor()
|
|
126
|
+
instrumentor.instrument(tracer_provider=self.tracer_provider)
|
|
127
|
+
self.instrumentors["langchain"] = instrumentor
|
|
128
|
+
logger.info("[OpenTelemetry Provider] Instrumented LangChain")
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error(f"Failed to instrument LangChain: {e}")
|
|
131
|
+
|
|
132
|
+
def instrument_pydantic_ai(self) -> None:
|
|
133
|
+
"""Instrument Pydantic AI"""
|
|
134
|
+
# Note: OpenLLMetry doesn't have a Pydantic AI instrumentation yet
|
|
135
|
+
# We'll need to create custom instrumentation or use manual spans
|
|
136
|
+
logger.info("[OpenTelemetry Provider] Pydantic AI instrumentation not yet available in OpenLLMetry")
|
|
137
|
+
|
|
138
|
+
@contextmanager
|
|
139
|
+
def trace_step(self, step_id: str, state: str = None, action: str = None, goal: str = None):
|
|
140
|
+
"""Context manager to associate spans with a specific step"""
|
|
141
|
+
span = self.tracer.start_span(
|
|
142
|
+
name=f"step.{step_id}",
|
|
143
|
+
attributes={
|
|
144
|
+
"lucidic.step_id": step_id,
|
|
145
|
+
"lucidic.step.state": state or "",
|
|
146
|
+
"lucidic.step.action": action or "",
|
|
147
|
+
"lucidic.step.goal": goal or ""
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
token = context.attach(trace.set_span_in_context(span))
|
|
152
|
+
try:
|
|
153
|
+
yield span
|
|
154
|
+
finally:
|
|
155
|
+
context.detach(token)
|
|
156
|
+
span.end()
|
|
157
|
+
|
|
158
|
+
def add_image_to_span(self, image_data: str, image_type: str = "screenshot") -> None:
|
|
159
|
+
"""Add image data to current span"""
|
|
160
|
+
current_span = trace.get_current_span()
|
|
161
|
+
if current_span and current_span.is_recording():
|
|
162
|
+
current_span.set_attribute(f"lucidic.image.{image_type}", image_data)
|
|
163
|
+
|
|
164
|
+
def set_step_context(self, step_id: str) -> None:
|
|
165
|
+
"""Set step ID in current span context"""
|
|
166
|
+
current_span = trace.get_current_span()
|
|
167
|
+
if current_span and current_span.is_recording():
|
|
168
|
+
current_span.set_attribute("lucidic.step_id", step_id)
|