fiddler-langgraph 1.0.0__tar.gz → 1.2.0__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 (23) hide show
  1. {fiddler_langgraph-1.0.0/fiddler_langgraph.egg-info → fiddler_langgraph-1.2.0}/PKG-INFO +1 -1
  2. fiddler_langgraph-1.2.0/fiddler_langgraph/VERSION +1 -0
  3. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/fiddler_langgraph/core/attributes.py +3 -0
  4. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/fiddler_langgraph/core/client.py +21 -24
  5. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/fiddler_langgraph/tracing/callback.py +139 -12
  6. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/fiddler_langgraph/tracing/jsonl_capture.py +8 -0
  7. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0/fiddler_langgraph.egg-info}/PKG-INFO +1 -1
  8. fiddler_langgraph-1.0.0/fiddler_langgraph/VERSION +0 -1
  9. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/MANIFEST.in +0 -0
  10. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/PUBLIC.md +0 -0
  11. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/README.md +0 -0
  12. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/fiddler_langgraph/__init__.py +0 -0
  13. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/fiddler_langgraph/core/__init__.py +0 -0
  14. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/fiddler_langgraph/core/span_processor.py +0 -0
  15. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/fiddler_langgraph/tracing/__init__.py +0 -0
  16. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/fiddler_langgraph/tracing/instrumentation.py +0 -0
  17. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/fiddler_langgraph/tracing/util.py +0 -0
  18. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/fiddler_langgraph.egg-info/SOURCES.txt +0 -0
  19. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/fiddler_langgraph.egg-info/dependency_links.txt +0 -0
  20. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/fiddler_langgraph.egg-info/requires.txt +0 -0
  21. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/fiddler_langgraph.egg-info/top_level.txt +0 -0
  22. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/pyproject.toml +0 -0
  23. {fiddler_langgraph-1.0.0 → fiddler_langgraph-1.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fiddler-langgraph
3
- Version: 1.0.0
3
+ Version: 1.2.0
4
4
  Summary: Python SDK for instrumenting GenAI Applications with Fiddler
5
5
  Home-page: https://fiddler.ai
6
6
  Author: Fiddler AI
@@ -0,0 +1 @@
1
+ 1.2.0
@@ -34,11 +34,14 @@ class FiddlerSpanAttributes: # pylint: disable=too-few-public-methods
34
34
  LLM_TOKEN_COUNT_INPUT = 'gen_ai.usage.input_tokens'
35
35
  LLM_TOKEN_COUNT_OUTPUT = 'gen_ai.usage.output_tokens'
36
36
  LLM_TOKEN_COUNT_TOTAL = 'gen_ai.usage.total_tokens'
37
+ GEN_AI_INPUT_MESSAGES = 'gen_ai.input.messages'
38
+ GEN_AI_OUTPUT_MESSAGES = 'gen_ai.output.messages'
37
39
 
38
40
  # tool attributes
39
41
  TOOL_INPUT = 'gen_ai.tool.input'
40
42
  TOOL_OUTPUT = 'gen_ai.tool.output'
41
43
  TOOL_NAME = 'gen_ai.tool.name'
44
+ TOOL_DEFINITIONS = 'gen_ai.tool.definitions'
42
45
 
43
46
 
44
47
  class FiddlerResourceAttributes:
@@ -1,6 +1,5 @@
1
1
  """Core client for Fiddler instrumentation."""
2
2
 
3
- import os
4
3
  import uuid
5
4
  from typing import Any
6
5
  from urllib.parse import urlparse
@@ -24,18 +23,6 @@ from fiddler_langgraph.core.attributes import FiddlerResourceAttributes
24
23
  from fiddler_langgraph.core.span_processor import FiddlerSpanProcessor
25
24
  from fiddler_langgraph.tracing.jsonl_capture import JSONLSpanExporter, initialize_jsonl_capture
26
25
 
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
26
 
40
27
  class FiddlerClient:
41
28
  """The main client for instrumenting Generative AI applications with Fiddler observability.
@@ -62,7 +49,7 @@ class FiddlerClient:
62
49
  application_id: str,
63
50
  url: str,
64
51
  console_tracer: bool = False,
65
- span_limits: SpanLimits | None = _default_span_limits,
52
+ span_limits: SpanLimits | None = None,
66
53
  sampler: sampling.Sampler | None = None,
67
54
  compression: Compression = Compression.Gzip,
68
55
  jsonl_capture_enabled: bool = False,
@@ -83,8 +70,18 @@ class FiddlerClient:
83
70
  instead of being sent to the Fiddler backend. Useful for debugging.
84
71
  Defaults to `False`.
85
72
  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 (32 events/links/attributes, 2048 char limit).
73
+ maximum number of attributes or events. When `None` (default), OpenTelemetry
74
+ automatically applies its standard defaults:
75
+
76
+ - `max_attributes`: 128 (or `OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT` env var)
77
+ - `max_events`: 128 (or `OTEL_SPAN_EVENT_COUNT_LIMIT` env var)
78
+ - `max_links`: 128 (or `OTEL_SPAN_LINK_COUNT_LIMIT` env var)
79
+ - `max_event_attributes`: 128 (or `OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT` env var)
80
+ - `max_link_attributes`: 128 (or `OTEL_LINK_ATTRIBUTE_COUNT_LIMIT` env var)
81
+ - `max_span_attribute_length`: None/unlimited (or `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var)
82
+
83
+ You can override these by passing a custom `SpanLimits` object (see example below)
84
+ or by setting the environment variables.
88
85
  sampler (sampling.Sampler | None): The sampler for deciding which spans to record.
89
86
  Defaults to `None`, which uses the parent-based always-on OpenTelemetry sampler
90
87
  (100% sampling).
@@ -120,11 +117,15 @@ class FiddlerClient:
120
117
  from opentelemetry.sdk.trace import SpanLimits, sampling
121
118
  from opentelemetry.exporter.otlp.proto.http.trace_exporter import Compression
122
119
 
120
+ # Example: add custom limits
123
121
  client = FiddlerClient(
124
122
  api_key='YOUR_API_KEY',
125
123
  application_id='YOUR_APPLICATION_ID',
126
124
  url='https://your-instance.fiddler.ai',
127
- span_limits=SpanLimits(max_span_attributes=64),
125
+ span_limits=SpanLimits(
126
+ max_span_attributes=64, # Reduce from default 128
127
+ max_span_attribute_length=2048, # Limit from default None (unlimited)
128
+ ),
128
129
  sampler=sampling.TraceIdRatioBased(0.1), # Sample 10% of traces
129
130
  compression=Compression.Gzip,
130
131
  )
@@ -304,13 +305,9 @@ class FiddlerClient:
304
305
  },
305
306
  compression=self.compression,
306
307
  )
307
- span_processor = BatchSpanProcessor(
308
- otlp_exporter,
309
- max_queue_size=int(os.environ.get('OTEL_BSP_MAX_QUEUE_SIZE', '100')),
310
- schedule_delay_millis=int(os.environ.get('OTEL_BSP_SCHEDULE_DELAY_MILLIS', '1000')),
311
- max_export_batch_size=int(os.environ.get('OTEL_BSP_MAX_EXPORT_BATCH_SIZE', '10')),
312
- export_timeout_millis=int(os.environ.get('OTEL_BSP_EXPORT_TIMEOUT', '5000')),
313
- )
308
+ # OpenTelemetry automatically applies defaults
309
+ # (OTEL_BSP_MAX_QUEUE_SIZE, OTEL_BSP_SCHEDULE_DELAY, OTEL_BSP_MAX_EXPORT_BATCH_SIZE, etc.)
310
+ span_processor = BatchSpanProcessor(otlp_exporter)
314
311
 
315
312
  self._provider.add_span_processor(span_processor)
316
313
 
@@ -9,7 +9,7 @@ from uuid import UUID
9
9
 
10
10
  from langchain_core.callbacks import BaseCallbackHandler
11
11
  from langchain_core.documents import Document
12
- from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
12
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage
13
13
  from langchain_core.outputs import ChatGeneration, LLMResult
14
14
  from opentelemetry import trace
15
15
  from opentelemetry.context.context import Context
@@ -138,6 +138,94 @@ def _set_token_usage_attributes(span: trace.Span, response: LLMResult) -> None:
138
138
  logger.warning('Failed to extract token usage: %s', e)
139
139
 
140
140
 
141
+ def _set_tool_definitions(span: trace.Span, kwargs: dict[str, Any]) -> None:
142
+ """Extract and set tool definitions on the span.
143
+
144
+ Retrieves tool definitions from invocation params and stores them as a
145
+ JSON-serialized string attribute on the span.
146
+
147
+ Parameters
148
+ ----------
149
+ span : trace.Span
150
+ The OpenTelemetry span to set attributes on
151
+ kwargs : dict[str, Any]
152
+ Callback kwargs containing invocation_params
153
+
154
+ """
155
+ try:
156
+ invocation_params = kwargs.get('invocation_params', {})
157
+ tools = invocation_params.get('tools')
158
+
159
+ if tools and isinstance(tools, list) and len(tools) > 0:
160
+ # Store tool definitions as-is in OpenAI native format
161
+ tool_definitions_json = json.dumps(tools, cls=_LanggraphJSONEncoder)
162
+ span.set_attribute(FiddlerSpanAttributes.TOOL_DEFINITIONS, tool_definitions_json)
163
+ except Exception as e:
164
+ logger.warning('Failed to extract tool definitions: %s', e)
165
+
166
+
167
+ def _convert_message_to_otel_format(message: BaseMessage) -> dict[str, Any]:
168
+ """Convert a LangChain message to OpenTelemetry format.
169
+
170
+ Parameters
171
+ ----------
172
+ message : BaseMessage
173
+ The LangChain message to convert
174
+
175
+ Returns
176
+ -------
177
+ dict[str, Any]
178
+ Message in OpenTelemetry format.
179
+
180
+ """
181
+ result: dict[str, Any] = {}
182
+
183
+ # Add OpenTelemetry role mapping
184
+ role_mapping = {'ai': 'assistant', 'human': 'user'}
185
+ result['role'] = role_mapping.get(message.type, message.type)
186
+
187
+ parts = []
188
+ content = _stringify_message_content(message)
189
+
190
+ # Handle ToolMessage separately
191
+ if isinstance(message, ToolMessage):
192
+ tool_response_part: dict[str, Any] = {
193
+ 'type': 'tool_call_response',
194
+ 'response': content,
195
+ 'id': message.tool_call_id,
196
+ }
197
+ parts = [tool_response_part]
198
+ else:
199
+ # Add text content if present
200
+ if content:
201
+ parts.append({'type': 'text', 'content': content})
202
+
203
+ # Add tool calls if present (for AIMessage)
204
+ if isinstance(message, AIMessage) and hasattr(message, 'tool_calls') and message.tool_calls:
205
+ for tool_call in message.tool_calls:
206
+ tool_call_part: dict[str, Any] = {
207
+ 'type': 'tool_call',
208
+ 'name': tool_call.get('name', ''),
209
+ }
210
+ if 'id' in tool_call:
211
+ tool_call_part['id'] = tool_call['id']
212
+ if 'args' in tool_call:
213
+ tool_call_part['arguments'] = tool_call['args']
214
+ parts.append(tool_call_part)
215
+
216
+ result['parts'] = parts
217
+
218
+ # Extract finish_reason
219
+ if isinstance(message, AIMessage) and hasattr(message, 'response_metadata'):
220
+ response_metadata = message.response_metadata
221
+ if response_metadata:
222
+ finish_reason = response_metadata.get('finish_reason')
223
+ if finish_reason:
224
+ result['finish_reason'] = finish_reason
225
+
226
+ return result
227
+
228
+
141
229
  class _CallbackHandler(BaseCallbackHandler):
142
230
  """A LangChain callback handler that creates OpenTelemetry spans for Fiddler.
143
231
 
@@ -397,6 +485,7 @@ class _CallbackHandler(BaseCallbackHandler):
397
485
  return
398
486
  child_span = self._create_child_span(parent_span, serialized.get('name', 'unknown'))
399
487
  span_input = json.dumps(inputs, cls=_LanggraphJSONEncoder) if inputs else input_str
488
+
400
489
  child_span.set_attribute(FiddlerSpanAttributes.TYPE, SpanType.TOOL)
401
490
  child_span.set_attribute(FiddlerSpanAttributes.TOOL_NAME, serialized.get('name', 'unknown'))
402
491
  child_span.set_attribute(FiddlerSpanAttributes.TOOL_INPUT, span_input)
@@ -428,7 +517,6 @@ class _CallbackHandler(BaseCallbackHandler):
428
517
  """
429
518
  span = self._get_span(run_id)
430
519
  if span:
431
- # limit the output to 100 characters for now - add formal limits later
432
520
  span.set_attribute(
433
521
  FiddlerSpanAttributes.TOOL_OUTPUT,
434
522
  json.dumps(output, cls=_LanggraphJSONEncoder),
@@ -623,14 +711,16 @@ class _CallbackHandler(BaseCallbackHandler):
623
711
 
624
712
  # chat models are a special case of LLMs with Structure Inputs (messages)
625
713
  # the ordering of messages is preserved over the lifecycle of an agent's invocation
626
- # we are ignoring AIMessage, ToolMessage, FunctionMessage & ChatMessage
627
- # see https://python.langchain.com/api_reference/core/messages.html#module-langchain_core.messages
628
714
  system_message = []
629
715
  user_message = []
716
+ message_history = []
717
+
630
718
  if messages and messages[0]:
631
719
  system_message = [m for m in messages[0] if isinstance(m, SystemMessage)]
632
720
  user_message = [m for m in messages[0] if isinstance(m, HumanMessage)]
633
721
 
722
+ message_history = list(messages[0])
723
+
634
724
  if metadata is not None:
635
725
  _set_agent_name(child_span, metadata)
636
726
 
@@ -641,6 +731,9 @@ class _CallbackHandler(BaseCallbackHandler):
641
731
  # Set model attributes
642
732
  _set_model_attributes(child_span, metadata)
643
733
 
734
+ # Extract and set tool definitions
735
+ _set_tool_definitions(child_span, kwargs)
736
+
644
737
  # We are only taking the 1st system message and 1st user message
645
738
  # as we are not supporting multiple system messages or multiple user messages
646
739
  # To support multiple system messages, we would need to add a new attribute with indexing
@@ -655,6 +748,16 @@ class _CallbackHandler(BaseCallbackHandler):
655
748
  FiddlerSpanAttributes.LLM_INPUT_USER,
656
749
  user_content,
657
750
  )
751
+
752
+ # Add complete message history as a span attribute (GenAI semantic convention)
753
+ if message_history:
754
+ # Convert messages to OpenTelemetry format
755
+ otel_messages = [_convert_message_to_otel_format(msg) for msg in message_history]
756
+ child_span.set_attribute(
757
+ FiddlerSpanAttributes.GEN_AI_INPUT_MESSAGES,
758
+ json.dumps(otel_messages, cls=_LanggraphJSONEncoder),
759
+ )
760
+
658
761
  self._set_session_id(child_span)
659
762
  self._add_span(child_span, run_id)
660
763
 
@@ -703,6 +806,9 @@ class _CallbackHandler(BaseCallbackHandler):
703
806
  # Set model attributes
704
807
  _set_model_attributes(child_span, metadata)
705
808
 
809
+ # Extract and set tool definitions
810
+ _set_tool_definitions(child_span, kwargs)
811
+
706
812
  # LLM model is more generic than a chat model, it only has a list on prompts
707
813
  # we are using the first prompt as both the system message and the user message
708
814
  # to capture all the prompts, we would need to add a new attribute with indexing
@@ -739,6 +845,8 @@ class _CallbackHandler(BaseCallbackHandler):
739
845
  # we always get only one element in the list - even with batch mode
740
846
  # Add safety checks to prevent index errors
741
847
  output = ''
848
+ output_message_dict = None
849
+
742
850
  if (
743
851
  response.generations
744
852
  and len(response.generations) > 0
@@ -746,22 +854,41 @@ class _CallbackHandler(BaseCallbackHandler):
746
854
  and len(response.generations[0]) > 0
747
855
  ):
748
856
  generation = response.generations[0][0]
857
+
749
858
  output = generation.text
750
- if (
751
- output == ''
752
- and isinstance(generation, ChatGeneration)
753
- and isinstance(generation.message, AIMessage)
754
- and hasattr(generation.message, 'tool_calls')
859
+
860
+ # Check if this is a ChatGeneration with an AIMessage
861
+ if isinstance(generation, ChatGeneration) and isinstance(
862
+ generation.message, AIMessage
755
863
  ):
756
- # if llm returns an empty string, it means it used a tool
757
- # we are using the tool calls to get the output
758
- output = json.dumps(generation.message.tool_calls, cls=_LanggraphJSONEncoder)
864
+ # Use the complete output message
865
+ output_message_dict = generation.message
866
+
867
+ if (
868
+ output == ''
869
+ and hasattr(generation.message, 'tool_calls')
870
+ and generation.message.tool_calls
871
+ ):
872
+ # if llm returns an empty string, it means it used a tool
873
+ # we are using the tool calls to get the output
874
+ output = json.dumps(
875
+ generation.message.tool_calls, cls=_LanggraphJSONEncoder
876
+ )
759
877
 
760
878
  span.set_attribute(FiddlerSpanAttributes.LLM_OUTPUT, output)
761
879
 
762
880
  # Extract and set token usage information
763
881
  _set_token_usage_attributes(span, response)
764
882
 
883
+ # Add output message as a span attribute
884
+ if output_message_dict:
885
+ # Convert message to OpenTelemetry format
886
+ otel_message = _convert_message_to_otel_format(output_message_dict)
887
+ span.set_attribute(
888
+ FiddlerSpanAttributes.GEN_AI_OUTPUT_MESSAGES,
889
+ json.dumps([otel_message], cls=_LanggraphJSONEncoder),
890
+ )
891
+
765
892
  span.end()
766
893
  self._remove_span(run_id)
767
894
  else:
@@ -98,11 +98,18 @@ class JSONLSpanCapture:
98
98
  span_data['llm_input_user'] = attributes.get(FiddlerSpanAttributes.LLM_INPUT_USER, '')
99
99
  span_data['llm_output'] = attributes.get(FiddlerSpanAttributes.LLM_OUTPUT, '')
100
100
  span_data['llm_context'] = attributes.get(FiddlerSpanAttributes.LLM_CONTEXT, '')
101
+ span_data['gen_ai_input_messages'] = attributes.get(
102
+ FiddlerSpanAttributes.GEN_AI_INPUT_MESSAGES, ''
103
+ )
104
+ span_data['gen_ai_output_messages'] = attributes.get(
105
+ FiddlerSpanAttributes.GEN_AI_OUTPUT_MESSAGES, ''
106
+ )
101
107
 
102
108
  # Tool information
103
109
  span_data['tool_name'] = attributes.get(FiddlerSpanAttributes.TOOL_NAME, '')
104
110
  span_data['tool_input'] = attributes.get(FiddlerSpanAttributes.TOOL_INPUT, '')
105
111
  span_data['tool_output'] = attributes.get(FiddlerSpanAttributes.TOOL_OUTPUT, '')
112
+ span_data['tool_definitions'] = attributes.get(FiddlerSpanAttributes.TOOL_DEFINITIONS, '')
106
113
 
107
114
  # Library versions (from resource if available)
108
115
  resource_attributes = (
@@ -124,6 +131,7 @@ class JSONLSpanCapture:
124
131
 
125
132
  # Exception information
126
133
  exception_info = []
134
+
127
135
  if hasattr(span, 'events') and span.events:
128
136
  for event in span.events:
129
137
  if event.name == 'exception':
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fiddler-langgraph
3
- Version: 1.0.0
3
+ Version: 1.2.0
4
4
  Summary: Python SDK for instrumenting GenAI Applications with Fiddler
5
5
  Home-page: https://fiddler.ai
6
6
  Author: Fiddler AI
@@ -1 +0,0 @@
1
- 1.0.0