fiddler-langgraph 0.1.0rc1__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.
@@ -0,0 +1 @@
1
+ 0.1.0rc1
@@ -0,0 +1,11 @@
1
+ """Fiddler SDK for instrumenting GenAI Applications."""
2
+
3
+ from pathlib import Path
4
+
5
+ from fiddler_langgraph.core.client import FiddlerClient
6
+
7
+ # Read version from VERSION file
8
+ _version_file = Path(__file__).parent / 'VERSION'
9
+ __version__ = _version_file.read_text().strip()
10
+
11
+ __all__ = ['FiddlerClient', '__version__']
@@ -0,0 +1 @@
1
+ """Core functionality for Fiddler SDK."""
@@ -0,0 +1,87 @@
1
+ """OpenTelemetry span attributes for Fiddler instrumentation."""
2
+
3
+ import contextvars
4
+ from typing import Any
5
+
6
+ from pydantic import ConfigDict, validate_call
7
+
8
+ # Key used for storing Fiddler-specific attributes in metadata dictionary
9
+ FIDDLER_METADATA_KEY = '_fiddler_attributes'
10
+
11
+ # Template strings for OpenTelemetry attribute key formatting
12
+ FIDDLER_USER_SPAN_ATTRIBUTE_TEMPLATE = 'fiddler.span.user.{key}'
13
+ FIDDLER_USER_SESSION_ATTRIBUTE_TEMPLATE = 'fiddler.session.user.{key}'
14
+
15
+
16
+ class FiddlerSpanAttributes: # pylint: disable=too-few-public-methods
17
+ """Constants for Fiddler OpenTelemetry span attributes."""
18
+
19
+ # common attributes
20
+ AGENT_NAME = 'gen_ai.agent.name'
21
+ AGENT_ID = 'gen_ai.agent.id'
22
+ CONVERSATION_ID = 'gen_ai.conversation.id'
23
+ TYPE = 'fiddler.span.type'
24
+
25
+ # LLM attributes
26
+ LLM_INPUT_SYSTEM = 'gen_ai.llm.input.system'
27
+ LLM_INPUT_USER = 'gen_ai.llm.input.user'
28
+ LLM_OUTPUT = 'gen_ai.llm.output'
29
+ LLM_CONTEXT = 'gen_ai.llm.context'
30
+
31
+ # Model attributes - following OpenTelemetry semantic conventions
32
+ LLM_REQUEST_MODEL = 'gen_ai.request.model'
33
+ LLM_SYSTEM = 'gen_ai.system'
34
+
35
+ # Token usage attributes
36
+ LLM_TOKEN_COUNT_INPUT = 'gen_ai.usage.input_tokens'
37
+ LLM_TOKEN_COUNT_OUTPUT = 'gen_ai.usage.output_tokens'
38
+ LLM_TOKEN_COUNT_TOTAL = 'gen_ai.usage.total_tokens'
39
+
40
+ # tool attributes
41
+ TOOL_INPUT = 'gen_ai.tool.input'
42
+ TOOL_OUTPUT = 'gen_ai.tool.output'
43
+ TOOL_NAME = 'gen_ai.tool.name'
44
+
45
+
46
+ class FiddlerResourceAttributes:
47
+ """Constants for Fiddler OpenTelemetry resource attributes."""
48
+
49
+ APPLICATION_ID = 'application.id'
50
+
51
+
52
+ class SpanType:
53
+ """Constants for Fiddler OpenTelemetry span types."""
54
+
55
+ CHAIN = 'chain'
56
+ TOOL = 'tool'
57
+ LLM = 'llm'
58
+ OTHER = 'other'
59
+
60
+
61
+ # context variable for conversation ID - used to store the conversation ID for the current thread/async coroutine
62
+ # note that contextvars are shallow copied, dictionaries/lists are not copied deeply and are shared between threads/coroutines
63
+ _CONVERSATION_ID: contextvars.ContextVar[str] = contextvars.ContextVar(
64
+ '_CONVERSATION_ID', default=''
65
+ )
66
+ _CUSTOM_ATTRIBUTES: contextvars.ContextVar[dict[str, Any]] = contextvars.ContextVar(
67
+ '_CUSTOM_ATTRIBUTES'
68
+ )
69
+
70
+
71
+ @validate_call(config=ConfigDict(strict=True, arbitrary_types_allowed=True))
72
+ def add_session_attributes(key: str, value: str) -> None:
73
+ """Adds Fiddler-specific attributes to a runnable's metadata.
74
+
75
+ This is used for various runnable types like Pregel nodes, LLM calls, tool
76
+ calls, and retriever calls.
77
+
78
+ Args:
79
+ key (str): The attribute key to add or update.
80
+ value (str): The attribute value to set.
81
+ """
82
+ try:
83
+ current_attributes = _CUSTOM_ATTRIBUTES.get().copy()
84
+ except LookupError:
85
+ current_attributes = {}
86
+ current_attributes[key] = value
87
+ _CUSTOM_ATTRIBUTES.set(current_attributes)
@@ -0,0 +1,318 @@
1
+ """Core client for Fiddler instrumentation."""
2
+
3
+ import os
4
+ import uuid
5
+ from typing import Any
6
+ from urllib.parse import urlparse
7
+
8
+ from opentelemetry import trace
9
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import Compression, OTLPSpanExporter
10
+ from opentelemetry.sdk.resources import (
11
+ OTELResourceDetector,
12
+ ProcessResourceDetector,
13
+ Resource,
14
+ get_aggregated_resources,
15
+ )
16
+ from opentelemetry.sdk.trace import SpanLimits, TracerProvider, sampling
17
+ from opentelemetry.sdk.trace.export import (
18
+ BatchSpanProcessor,
19
+ ConsoleSpanExporter,
20
+ SimpleSpanProcessor,
21
+ )
22
+
23
+ from fiddler_langgraph.core.attributes import FiddlerResourceAttributes
24
+ from fiddler_langgraph.core.span_processor import FiddlerSpanProcessor
25
+ from fiddler_langgraph.tracing.jsonl_capture import JSONLSpanExporter, initialize_jsonl_capture
26
+
27
+ # Defaults are too permissive.
28
+ # Set restrictive defaults for span limits - can be overridden by the user
29
+ # See https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py
30
+ _default_span_limits = SpanLimits(
31
+ max_events=32,
32
+ max_links=32,
33
+ max_span_attributes=32,
34
+ max_event_attributes=32,
35
+ max_link_attributes=32,
36
+ max_span_attribute_length=2048,
37
+ )
38
+
39
+
40
+ class FiddlerClient:
41
+ """The main client for instrumenting Generative AI applications with Fiddler observability.
42
+
43
+ This client configures and manages the OpenTelemetry tracer that sends telemetry data
44
+ to the Fiddler platform for monitoring, analysis, and debugging of your AI agents
45
+ and workflows.
46
+
47
+ Attributes:
48
+ application_id (str): The UUID4 identifier for the application.
49
+ url (str): The Fiddler backend URL.
50
+ api_key (str): The API key for Fiddler.
51
+ resource (Resource): The OpenTelemetry resource for the client.
52
+ span_limits (SpanLimits | None): OpenTelemetry span limits configuration.
53
+ sampler (sampling.Sampler | None): OpenTelemetry sampling configuration.
54
+ compression (Compression): OTLP export compression type.
55
+ jsonl_capture_enabled (bool): Whether JSONL capture is enabled.
56
+ jsonl_file_path (str): Path to the JSONL file for trace data capture.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ api_key: str,
62
+ application_id: str,
63
+ url: str = 'http://localhost:4318',
64
+ console_tracer: bool = False,
65
+ span_limits: SpanLimits | None = _default_span_limits,
66
+ sampler: sampling.Sampler | None = None,
67
+ compression: Compression = Compression.Gzip,
68
+ jsonl_capture_enabled: bool = False,
69
+ jsonl_file_path: str = 'fiddler_trace_data.jsonl',
70
+ ):
71
+ """Initializes the FiddlerClient.
72
+
73
+ This sets up the configuration for the OpenTelemetry tracer that will
74
+ be used to send data to Fiddler.
75
+
76
+ Args:
77
+ api_key (str): The API key for authenticating with the Fiddler backend. **Required**.
78
+ application_id (str): The unique identifier (UUID4) for the application. **Required**.
79
+ url (str): The base URL for the Fiddler backend. While it defaults to
80
+ `http://localhost:4318` for local development, this **must** be set to your
81
+ Fiddler instance URL for any other use.
82
+ console_tracer (bool): If True, traces will be printed to the console
83
+ instead of being sent to the Fiddler backend. Useful for debugging.
84
+ Defaults to `False`.
85
+ span_limits (SpanLimits | None): Configuration for span limits, such as the
86
+ maximum number of attributes or events. Defaults to a restrictive
87
+ set of internal limits.
88
+ sampler (sampling.Sampler | None): The sampler for deciding which spans to record.
89
+ Defaults to `None`, which uses the parent-based OpenTelemetry sampler.
90
+ compression (Compression): The compression for exporting traces.
91
+ Can be `Compression.Gzip` or `Compression.NoCompression`.
92
+ Defaults to `Compression.Gzip`.
93
+ jsonl_capture_enabled (bool): Whether to enable JSONL capture of trace data.
94
+ When enabled, all span data will be captured and saved to a JSONL file
95
+ in OpenTelemetry format for analysis. Defaults to `False`.
96
+ jsonl_file_path (str): Path to the JSONL file where trace data will be saved.
97
+ Only used when `jsonl_capture_enabled` is `True`. Defaults to
98
+ "fiddler_trace_data.jsonl".
99
+
100
+ Raises:
101
+ ValueError: If `application_id` is not a valid UUID4 or if the
102
+ `url` is not a valid HTTPS URL.
103
+
104
+ Examples:
105
+ >>> from opentelemetry.sdk.trace import SpanLimits
106
+ >>> from fiddler_langgraph import FiddlerClient
107
+ >>>
108
+ >>> client = FiddlerClient(
109
+ ... api_key='YOUR_API_KEY',
110
+ ... application_id='YOUR_APPLICATION_ID',
111
+ ... url='https://your-fiddler-instance.fiddler.ai',
112
+ ... span_limits=SpanLimits(max_span_attributes=64),
113
+ ... )
114
+ """
115
+ # Validate application_id is a valid UUID4
116
+
117
+ parsed_uuid = uuid.UUID(application_id)
118
+ if parsed_uuid.version != 4:
119
+ raise ValueError(
120
+ f'application_id must be a valid UUID4 (version 4), got version {parsed_uuid.version}'
121
+ )
122
+ # Store the validated UUID as a string
123
+ self.application_id = str(parsed_uuid)
124
+
125
+ # Validate URL is a valid URL format
126
+ parsed_url = urlparse(url)
127
+ if not parsed_url.scheme or not parsed_url.netloc:
128
+ raise ValueError('URL must have a valid scheme and netloc')
129
+ if parsed_url.scheme not in ('http', 'https'):
130
+ raise ValueError('URL scheme must be http or https')
131
+ self.url = url.rstrip('/')
132
+
133
+ self.api_key = api_key
134
+
135
+ # fiddler sdk must have its own tracer provider and tracer
136
+ # so we can have a separate configuration for the tracer provider than the global one.
137
+ # Additionally, other otel libraries maybe active who may override configs of the global tracer provider.
138
+ # we will initialize the provider and tracer when get_tracer is called
139
+ # we need to wait for any resources to be set before initializing the provider
140
+ # and tracer
141
+ self._provider: TracerProvider | None = None
142
+ self._tracer: trace.Tracer | None = None
143
+ self._console_tracer = console_tracer
144
+
145
+ self.span_limits = span_limits
146
+ self.sampler = sampler
147
+ self.compression = compression
148
+ self.jsonl_capture_enabled = jsonl_capture_enabled
149
+ self.jsonl_file_path = jsonl_file_path
150
+
151
+ # Create OpenTelemetry resource with service information
152
+ # we will update the resource with any additional attributes later
153
+ resource = Resource.create({FiddlerResourceAttributes.APPLICATION_ID: self.application_id})
154
+ self.resource = self._get_aggregated_resources_with_fallback(resource)
155
+
156
+ def get_tracer_provider(self) -> TracerProvider:
157
+ """Gets the OpenTelemetry TracerProvider instance.
158
+
159
+ Initializes the provider on the first call.
160
+
161
+ Returns:
162
+ TracerProvider: The configured OpenTelemetry TracerProvider.
163
+
164
+ Raises:
165
+ RuntimeError: If tracer provider initialization fails.
166
+ """
167
+ if self._provider is None:
168
+ self._initialize_provider()
169
+ if self._provider is None:
170
+ raise RuntimeError('Failed to initialize tracer provider')
171
+ return self._provider
172
+
173
+ def _get_aggregated_resources_with_fallback(self, initial_resource: Resource) -> Resource:
174
+ """Gets aggregated resources with a fallback for different OpenTelemetry versions.
175
+
176
+ This method tries to use `get_aggregated_resources` and dynamically imports
177
+ `OsResourceDetector` if available. It falls back to the initial resource if
178
+ aggregation fails.
179
+
180
+ Args:
181
+ initial_resource (Resource): The initial resource to start with.
182
+
183
+ Returns:
184
+ Resource: The aggregated resource.
185
+ """
186
+ detectors = [OTELResourceDetector(), ProcessResourceDetector()]
187
+
188
+ # Try to add OsResourceDetector if available (OpenTelemetry >= 1.19)
189
+ try:
190
+ from opentelemetry.sdk.resources import OsResourceDetector
191
+
192
+ detectors.append(OsResourceDetector())
193
+ except ImportError:
194
+ # OsResourceDetector not available in this version, skip it
195
+ pass
196
+
197
+ try:
198
+ return get_aggregated_resources(detectors, initial_resource=initial_resource)
199
+ except Exception:
200
+ # Fallback to initial resource if aggregation fails
201
+ return initial_resource
202
+
203
+ def update_resource(self, attributes: dict[str, Any]) -> None:
204
+ """Updates the OpenTelemetry resource with additional attributes.
205
+
206
+ Use this to add metadata that applies to all spans, such as version numbers
207
+ or environment names.
208
+
209
+ > [!IMPORTANT]
210
+ > Must be called before `get_tracer()` is invoked.
211
+
212
+ Args:
213
+ attributes (dict[str, Any]): Key-value pairs to add to the resource. **Required**.
214
+
215
+ Raises:
216
+ ValueError: If the tracer has already been initialized.
217
+
218
+ Examples:
219
+ >>> from fiddler_langgraph import FiddlerClient
220
+ >>> client = FiddlerClient(api_key='...', application_id='...')
221
+ >>> client.update_resource({'service.version': '1.2.3'})
222
+ """
223
+ if self._tracer is not None:
224
+ raise ValueError('Cannot update resource after tracer is initialized')
225
+
226
+ if (
227
+ self.resource.attributes.get('service.name', '') != 'unknown_service'
228
+ and attributes.get('service.name') is None
229
+ ):
230
+ # service.name defaults to unknown_service in a new resource. When merging, the new resource will override the old one.
231
+ # so we need to keep the old service.name if it exists.
232
+ attributes['service.name'] = self.resource.attributes['service.name']
233
+
234
+ self.resource = self.resource.merge(Resource.create(attributes))
235
+
236
+ def _initialize_provider(self) -> None:
237
+ """Initializes the tracer provider.
238
+
239
+ We are not using the default tracer provider because we want to have a
240
+ separate configuration for the tracer provider than the global one.
241
+ Additionally, other OTEL libraries may be active and override configs
242
+ of the global tracer provider.
243
+ """
244
+ if self._provider is not None:
245
+ return
246
+
247
+ self._provider = TracerProvider(
248
+ resource=self.resource,
249
+ span_limits=self.span_limits,
250
+ sampler=self.sampler,
251
+ )
252
+
253
+ def _initialize_tracer(self) -> None:
254
+ """Initializes the OpenTelemetry tracer and registers span processors."""
255
+ if self._tracer is not None:
256
+ return
257
+
258
+ # Ensure provider is initialized
259
+ self._initialize_provider()
260
+ assert self._provider is not None # Type guard for mypy
261
+
262
+ # processors are executed in order, so we add the FiddlerSpanProcessor first
263
+ # so that it can inject the session ID and custom attributes into the spans
264
+ self._provider.add_span_processor(FiddlerSpanProcessor())
265
+
266
+ if self._console_tracer:
267
+ self._provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
268
+
269
+ otlp_exporter = OTLPSpanExporter(
270
+ endpoint=f'{self.url}/v1/traces',
271
+ headers={
272
+ 'authorization': f'Bearer {self.api_key}',
273
+ 'fiddler-application-id': self.application_id,
274
+ },
275
+ compression=self.compression,
276
+ )
277
+ span_processor = BatchSpanProcessor(
278
+ otlp_exporter,
279
+ max_queue_size=int(os.environ.get('OTEL_BSP_MAX_QUEUE_SIZE', '100')),
280
+ schedule_delay_millis=int(os.environ.get('OTEL_BSP_SCHEDULE_DELAY_MILLIS', '1000')),
281
+ max_export_batch_size=int(os.environ.get('OTEL_BSP_MAX_EXPORT_BATCH_SIZE', '10')),
282
+ export_timeout_millis=int(os.environ.get('OTEL_BSP_EXPORT_TIMEOUT', '5000')),
283
+ )
284
+
285
+ self._provider.add_span_processor(span_processor)
286
+
287
+ # Add JSONL capture if enabled
288
+ if self.jsonl_capture_enabled:
289
+ jsonl_capture = initialize_jsonl_capture(self.jsonl_file_path)
290
+ jsonl_exporter = JSONLSpanExporter(jsonl_capture)
291
+ self._provider.add_span_processor(SimpleSpanProcessor(jsonl_exporter))
292
+
293
+ self._tracer = trace.get_tracer('fiddler.langgraph.tracer', tracer_provider=self._provider)
294
+
295
+ def get_tracer(self) -> trace.Tracer:
296
+ """Returns an OpenTelemetry tracer instance for creating spans.
297
+
298
+ Initializes the tracer on the first call. This is the primary method
299
+ for developers to get a tracer for custom instrumentation.
300
+
301
+ Returns:
302
+ trace.Tracer: OpenTelemetry tracer instance.
303
+
304
+ Raises:
305
+ RuntimeError: If tracer initialization fails.
306
+
307
+ Examples:
308
+ >>> from fiddler_langgraph import FiddlerClient
309
+ >>> client = FiddlerClient(api_key='...', application_id='...')
310
+ >>> tracer = client.get_tracer()
311
+ >>> with tracer.start_as_current_span('my-operation'):
312
+ ... print('Doing some work...')
313
+ """
314
+ if self._tracer is None:
315
+ self._initialize_tracer()
316
+ if self._tracer is None:
317
+ raise RuntimeError('Failed to initialize tracer')
318
+ return self._tracer
@@ -0,0 +1,31 @@
1
+ from opentelemetry import context
2
+ from opentelemetry.sdk.trace import SpanProcessor
3
+ from opentelemetry.trace import Span
4
+
5
+ from fiddler_langgraph.core.attributes import (
6
+ _CONVERSATION_ID,
7
+ _CUSTOM_ATTRIBUTES,
8
+ FIDDLER_USER_SESSION_ATTRIBUTE_TEMPLATE,
9
+ FiddlerSpanAttributes,
10
+ )
11
+
12
+
13
+ class FiddlerSpanProcessor(SpanProcessor):
14
+ def on_start(self, span: Span, parent_context: context.Context | None = None):
15
+ # inject custom attributes
16
+ try:
17
+ custom_attributes = _CUSTOM_ATTRIBUTES.get().copy()
18
+ except LookupError:
19
+ # LookupError is raised if the contextvar is not set
20
+ custom_attributes = {}
21
+ if custom_attributes:
22
+ for key, value in custom_attributes.items():
23
+ # prefix the key with fiddler.session.
24
+ # fdl_key = f'fiddler.session.{key}'
25
+ fdl_key = FIDDLER_USER_SESSION_ATTRIBUTE_TEMPLATE.format(key=key)
26
+ span.set_attribute(fdl_key, value)
27
+
28
+ # inject session id
29
+ session_id = _CONVERSATION_ID.get()
30
+ if session_id:
31
+ span.set_attribute(FiddlerSpanAttributes.CONVERSATION_ID, session_id)
@@ -0,0 +1 @@
1
+ """LangGraph instrumentation for Fiddler SDK."""