langtrace-python-sdk 3.8.6__py3-none-any.whl → 3.8.8__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.
@@ -22,6 +22,7 @@ from .litellm import LiteLLMInstrumentation
22
22
  from .llamaindex import LlamaindexInstrumentation
23
23
  from .milvus import MilvusInstrumentation
24
24
  from .mistral import MistralInstrumentation
25
+ from .neo4j_graphrag import Neo4jGraphRAGInstrumentation
25
26
  from .ollama import OllamaInstrumentor
26
27
  from .openai import OpenAIInstrumentation
27
28
  from .openai_agents import OpenAIAgentsInstrumentation
@@ -59,6 +60,7 @@ __all__ = [
59
60
  "AWSBedrockInstrumentation",
60
61
  "CerebrasInstrumentation",
61
62
  "MilvusInstrumentation",
63
+ "Neo4jGraphRAGInstrumentation",
62
64
  "GoogleGenaiInstrumentation",
63
65
  "CrewaiToolsInstrumentation",
64
66
  "GraphlitInstrumentation",
@@ -0,0 +1,3 @@
1
+ from .instrumentation import Neo4jGraphRAGInstrumentation
2
+
3
+ __all__ = ["Neo4jGraphRAGInstrumentation"]
@@ -0,0 +1,62 @@
1
+ """
2
+ Copyright (c) 2025 Scale3 Labs
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
16
+
17
+ from typing import Collection
18
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
19
+ from opentelemetry import trace
20
+ from wrapt import wrap_function_wrapper as _W
21
+ from importlib.metadata import version as v
22
+ from .patch import patch_graphrag_search, patch_kg_pipeline_run, \
23
+ patch_kg_pipeline_run, patch_retriever_search
24
+
25
+
26
+ class Neo4jGraphRAGInstrumentation(BaseInstrumentor):
27
+
28
+ def instrumentation_dependencies(self) -> Collection[str]:
29
+ return ["neo4j-graphrag>=1.6.0"]
30
+
31
+ def _instrument(self, **kwargs):
32
+ tracer_provider = kwargs.get("tracer_provider")
33
+ tracer = trace.get_tracer(__name__, "", tracer_provider)
34
+ graphrag_version = v("neo4j-graphrag")
35
+
36
+ try:
37
+ # instrument kg builder
38
+ _W(
39
+ "neo4j_graphrag.experimental.pipeline.kg_builder",
40
+ "SimpleKGPipeline.run_async",
41
+ patch_kg_pipeline_run("run_async", graphrag_version, tracer),
42
+ )
43
+
44
+ # Instrument GraphRAG
45
+ _W(
46
+ "neo4j_graphrag.generation.graphrag",
47
+ "GraphRAG.search",
48
+ patch_graphrag_search("search", graphrag_version, tracer),
49
+ )
50
+
51
+ # Instrument retrievers
52
+ _W(
53
+ "neo4j_graphrag.retrievers.vector",
54
+ "VectorRetriever.get_search_results",
55
+ patch_retriever_search("vector_search", graphrag_version, tracer),
56
+ )
57
+
58
+ except Exception as e:
59
+ print(f"Failed to instrument Neo4j GraphRAG: {e}")
60
+
61
+ def _uninstrument(self, **kwargs):
62
+ pass
@@ -0,0 +1,229 @@
1
+ """
2
+ Copyright (c) 2025 Scale3 Labs
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
16
+
17
+ import json
18
+
19
+ from importlib_metadata import version as v
20
+ from langtrace.trace_attributes import FrameworkSpanAttributes
21
+ from opentelemetry import baggage
22
+ from opentelemetry.trace import Span, SpanKind, Tracer
23
+ from opentelemetry.trace.status import Status, StatusCode
24
+
25
+ from langtrace_python_sdk.constants import LANGTRACE_SDK_NAME
26
+ from langtrace_python_sdk.constants.instrumentation.common import (
27
+ LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY, SERVICE_PROVIDERS)
28
+ from langtrace_python_sdk.utils.llm import set_span_attributes
29
+ from langtrace_python_sdk.utils.misc import serialize_args, serialize_kwargs
30
+
31
+
32
+ def patch_kg_pipeline_run(operation_name: str, version: str, tracer: Tracer):
33
+
34
+ async def async_traced_method(wrapped, instance, args, kwargs):
35
+ service_provider = SERVICE_PROVIDERS.get("NEO4J_GRAPHRAG", "neo4j_graphrag")
36
+ extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY)
37
+
38
+ span_attributes = {
39
+ "langtrace.sdk.name": "langtrace-python-sdk",
40
+ "langtrace.service.name": service_provider,
41
+ "langtrace.service.type": "framework",
42
+ "langtrace.service.version": version,
43
+ "langtrace.version": v(LANGTRACE_SDK_NAME),
44
+ "neo4j.pipeline.type": "SimpleKGPipeline",
45
+ **(extra_attributes if extra_attributes is not None else {}),
46
+ }
47
+
48
+ if len(args) > 0:
49
+ span_attributes["neo4j.pipeline.inputs"] = serialize_args(*args)
50
+ if kwargs:
51
+ span_attributes["neo4j.pipeline.kwargs"] = serialize_kwargs(**kwargs)
52
+
53
+ file_path = kwargs.get("file_path", args[0] if len(args) > 0 else None)
54
+ text = kwargs.get("text", args[1] if len(args) > 1 else None)
55
+ if file_path:
56
+ span_attributes["neo4j.pipeline.file_path"] = file_path
57
+ if text:
58
+ span_attributes["neo4j.pipeline.text_length"] = len(text)
59
+
60
+ if hasattr(instance, "runner") and hasattr(instance.runner, "config"):
61
+ config = instance.runner.config
62
+ if config:
63
+ span_attributes["neo4j.pipeline.from_pdf"] = getattr(config, "from_pdf", None)
64
+ span_attributes["neo4j.pipeline.perform_entity_resolution"] = getattr(config, "perform_entity_resolution", None)
65
+
66
+ attributes = FrameworkSpanAttributes(**span_attributes)
67
+
68
+ with tracer.start_as_current_span(
69
+ name=f"neo4j.pipeline.{operation_name}",
70
+ kind=SpanKind.CLIENT,
71
+ ) as span:
72
+ try:
73
+ set_span_attributes(span, attributes)
74
+
75
+ result = await wrapped(*args, **kwargs)
76
+
77
+ if result:
78
+ try:
79
+ if hasattr(result, "to_dict"):
80
+ result_dict = result.to_dict()
81
+ span.set_attribute("neo4j.pipeline.result", json.dumps(result_dict))
82
+ elif hasattr(result, "model_dump"):
83
+ result_dict = result.model_dump()
84
+ span.set_attribute("neo4j.pipeline.result", json.dumps(result_dict))
85
+ except Exception as e:
86
+ span.set_attribute("neo4j.pipeline.result_error", str(e))
87
+
88
+ span.set_status(Status(StatusCode.OK))
89
+ return result
90
+
91
+ except Exception as err:
92
+ span.record_exception(err)
93
+ span.set_status(Status(StatusCode.ERROR, str(err)))
94
+ raise
95
+
96
+ return async_traced_method
97
+
98
+
99
+ def patch_graphrag_search(operation_name: str, version: str, tracer: Tracer):
100
+
101
+ def traced_method(wrapped, instance, args, kwargs):
102
+ service_provider = SERVICE_PROVIDERS.get("NEO4J_GRAPHRAG", "neo4j_graphrag")
103
+ extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY)
104
+
105
+ # Basic attributes
106
+ span_attributes = {
107
+ "langtrace.sdk.name": "langtrace-python-sdk",
108
+ "langtrace.service.name": service_provider,
109
+ "langtrace.service.type": "framework",
110
+ "langtrace.service.version": version,
111
+ "langtrace.version": v(LANGTRACE_SDK_NAME),
112
+ "neo4j_graphrag.operation": operation_name,
113
+ **(extra_attributes if extra_attributes is not None else {}),
114
+ }
115
+
116
+ query_text = kwargs.get("query_text", args[0] if len(args) > 0 else None)
117
+ if query_text:
118
+ span_attributes["neo4j_graphrag.query_text"] = query_text
119
+
120
+ retriever_config = kwargs.get("retriever_config", None)
121
+ if retriever_config:
122
+ span_attributes["neo4j_graphrag.retriever_config"] = json.dumps(retriever_config)
123
+
124
+ if hasattr(instance, "retriever"):
125
+ span_attributes["neo4j_graphrag.retriever_type"] = instance.retriever.__class__.__name__
126
+
127
+ if hasattr(instance, "llm"):
128
+ span_attributes["neo4j_graphrag.llm_type"] = instance.llm.__class__.__name__
129
+
130
+ attributes = FrameworkSpanAttributes(**span_attributes)
131
+
132
+ with tracer.start_as_current_span(
133
+ name=f"neo4j_graphrag.{operation_name}",
134
+ kind=SpanKind.CLIENT,
135
+ ) as span:
136
+ try:
137
+ set_span_attributes(span, attributes)
138
+
139
+ result = wrapped(*args, **kwargs)
140
+
141
+ if result and hasattr(result, "answer"):
142
+ span.set_attribute("neo4j_graphrag.answer", result.answer)
143
+
144
+ if hasattr(result, "retriever_result") and result.retriever_result:
145
+ try:
146
+ retriever_items = len(result.retriever_result.items)
147
+ span.set_attribute("neo4j_graphrag.context_items", retriever_items)
148
+ except Exception:
149
+ pass
150
+
151
+ span.set_status(Status(StatusCode.OK))
152
+ return result
153
+
154
+ except Exception as err:
155
+ span.record_exception(err)
156
+ span.set_status(Status(StatusCode.ERROR, str(err)))
157
+ raise
158
+
159
+ return traced_method
160
+
161
+
162
+ def patch_retriever_search(operation_name: str, version: str, tracer: Tracer):
163
+
164
+ def traced_method(wrapped, instance, args, kwargs):
165
+ service_provider = SERVICE_PROVIDERS.get("NEO4J_GRAPHRAG", "neo4j_graphrag")
166
+ extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY)
167
+
168
+ # Basic attributes
169
+ span_attributes = {
170
+ "langtrace.sdk.name": "langtrace-python-sdk",
171
+ "langtrace.service.name": service_provider,
172
+ "langtrace.service.type": "framework",
173
+ "langtrace.service.version": version,
174
+ "langtrace.version": v(LANGTRACE_SDK_NAME),
175
+ "neo4j.retriever.operation": operation_name,
176
+ "neo4j.retriever.type": instance.__class__.__name__,
177
+ **(extra_attributes if extra_attributes is not None else {}),
178
+ }
179
+
180
+ query_text = kwargs.get("query_text", args[0] if len(args) > 0 else None)
181
+ if query_text:
182
+ span_attributes["neo4j.retriever.query_text"] = query_text
183
+
184
+ if hasattr(instance, "__class__") and hasattr(instance.__class__, "__name__"):
185
+ retriever_type = instance.__class__.__name__
186
+
187
+ if retriever_type == "VectorRetriever" and hasattr(instance, "index_name"):
188
+ span_attributes["neo4j.vector_retriever.index_name"] = instance.index_name
189
+
190
+ if retriever_type == "KnowledgeGraphRetriever" and hasattr(instance, "cypher_query"):
191
+ span_attributes["neo4j.kg_retriever.cypher_query"] = instance.cypher_query
192
+
193
+ for param in ["top_k", "similarity_threshold"]:
194
+ if param in kwargs:
195
+ span_attributes[f"neo4j.retriever.{param}"] = kwargs[param]
196
+ elif hasattr(instance, param):
197
+ span_attributes[f"neo4j.retriever.{param}"] = getattr(instance, param)
198
+
199
+ attributes = FrameworkSpanAttributes(**span_attributes)
200
+
201
+ with tracer.start_as_current_span(
202
+ name=f"neo4j.retriever.{operation_name}",
203
+ kind=SpanKind.CLIENT,
204
+ ) as span:
205
+ try:
206
+ set_span_attributes(span, attributes)
207
+
208
+ result = wrapped(*args, **kwargs)
209
+
210
+ if result:
211
+ if hasattr(result, "items") and isinstance(result.items, list):
212
+ span.set_attribute("neo4j.retriever.items_count", len(result.items))
213
+
214
+ try:
215
+ item_ids = [item.id for item in result.items[:5] if hasattr(item, "id")]
216
+ if item_ids:
217
+ span.set_attribute("neo4j.retriever.item_ids", json.dumps(item_ids))
218
+ except Exception:
219
+ pass
220
+
221
+ span.set_status(Status(StatusCode.OK))
222
+ return result
223
+
224
+ except Exception as err:
225
+ span.record_exception(err)
226
+ span.set_status(Status(StatusCode.ERROR, str(err)))
227
+ raise
228
+
229
+ return traced_method
@@ -24,6 +24,8 @@ from langtrace_python_sdk.instrumentation.openai.patch import (
24
24
  async_embeddings_create,
25
25
  async_images_generate,
26
26
  chat_completions_create,
27
+ openai_responses_create,
28
+ async_openai_responses_create,
27
29
  embeddings_create,
28
30
  images_edit,
29
31
  images_generate,
@@ -32,7 +34,7 @@ from langtrace_python_sdk.instrumentation.openai.patch import (
32
34
  logging.basicConfig(level=logging.FATAL)
33
35
 
34
36
 
35
- class OpenAIInstrumentation(BaseInstrumentor): # type: ignore
37
+ class OpenAIInstrumentation(BaseInstrumentor): # type: ignore
36
38
 
37
39
  def instrumentation_dependencies(self) -> Collection[str]:
38
40
  return ["openai >= 0.27.0", "trace-attributes >= 4.0.5"]
@@ -54,6 +56,18 @@ class OpenAIInstrumentation(BaseInstrumentor): # type: ignore
54
56
  async_chat_completions_create(version, tracer),
55
57
  )
56
58
 
59
+ wrap_function_wrapper(
60
+ "openai.resources.responses",
61
+ "AsyncResponses.create",
62
+ async_openai_responses_create(version, tracer),
63
+ )
64
+
65
+ wrap_function_wrapper(
66
+ "openai.resources.responses",
67
+ "Responses.create",
68
+ openai_responses_create(version, tracer),
69
+ )
70
+
57
71
  wrap_function_wrapper(
58
72
  "openai.resources.images",
59
73
  "Images.generate",
@@ -7,27 +7,121 @@ from opentelemetry.trace import Span, SpanKind, Tracer
7
7
  from opentelemetry.trace.propagation import set_span_in_context
8
8
  from opentelemetry.trace.status import Status, StatusCode
9
9
 
10
- from langtrace_python_sdk.constants.instrumentation.common import \
11
- SERVICE_PROVIDERS
10
+ from langtrace_python_sdk.constants.instrumentation.common import SERVICE_PROVIDERS
12
11
  from langtrace_python_sdk.constants.instrumentation.openai import APIS
13
12
  from langtrace_python_sdk.instrumentation.openai.types import (
14
- ChatCompletionsCreateKwargs, ContentItem, EmbeddingsCreateKwargs,
15
- ImagesEditKwargs, ImagesGenerateKwargs, ResultType)
13
+ ChatCompletionsCreateKwargs,
14
+ ContentItem,
15
+ EmbeddingsCreateKwargs,
16
+ ImagesEditKwargs,
17
+ ImagesGenerateKwargs,
18
+ ResultType,
19
+ )
16
20
  from langtrace_python_sdk.types import NOT_GIVEN
17
21
  from langtrace_python_sdk.utils import set_span_attribute
18
- from langtrace_python_sdk.utils.llm import (StreamWrapper,
19
- calculate_prompt_tokens,
20
- get_base_url, get_extra_attributes,
21
- get_langtrace_attributes,
22
- get_llm_request_attributes,
23
- get_llm_url, get_span_name,
24
- get_tool_calls, is_streaming,
25
- set_event_completion,
26
- set_span_attributes,
27
- set_usage_attributes)
22
+ from langtrace_python_sdk.utils.llm import (
23
+ StreamWrapper,
24
+ calculate_prompt_tokens,
25
+ get_base_url,
26
+ get_extra_attributes,
27
+ get_langtrace_attributes,
28
+ get_llm_request_attributes,
29
+ get_llm_url,
30
+ get_span_name,
31
+ get_tool_calls,
32
+ is_streaming,
33
+ set_event_completion,
34
+ set_span_attributes,
35
+ set_usage_attributes,
36
+ )
28
37
  from langtrace_python_sdk.utils.silently_fail import silently_fail
29
38
 
30
39
 
40
+ def async_openai_responses_create(version: str, tracer: Tracer) -> Callable:
41
+ """Wrap the `create` method of the `openai.AsyncResponse.create` class to trace it."""
42
+
43
+ async def traced_method(
44
+ wrapped: Callable, instance: Any, args: List[Any], kwargs: Dict[str, Any]
45
+ ):
46
+ input_value = kwargs.get("input")
47
+ prompt = (
48
+ input_value[0]
49
+ if isinstance(input_value, list)
50
+ else [{"role": "user", "content": input_value}]
51
+ )
52
+ service_provider = SERVICE_PROVIDERS["OPENAI"]
53
+ span_attributes = {
54
+ "instructions": kwargs.get("instructions"),
55
+ **get_langtrace_attributes(version, service_provider, vendor_type="llm"),
56
+ **get_llm_request_attributes(
57
+ kwargs,
58
+ operation_name="openai.responses.create",
59
+ prompts=prompt,
60
+ ),
61
+ }
62
+ with tracer.start_as_current_span(
63
+ name="openai.responses.create",
64
+ kind=SpanKind.CLIENT,
65
+ context=set_span_in_context(trace.get_current_span()),
66
+ ) as span:
67
+ try:
68
+ set_span_attributes(span, span_attributes)
69
+
70
+ response = await wrapped(*args, **kwargs)
71
+ _set_openai_agentic_response_attributes(span, response)
72
+
73
+ return response
74
+ except Exception as err:
75
+ span.record_exception(err)
76
+ raise
77
+
78
+ return traced_method
79
+
80
+
81
+ def openai_responses_create(version: str, tracer: Tracer) -> Callable:
82
+ """Wrap the `create` method of the `openai.responses.create` class to trace it."""
83
+
84
+ def traced_method(
85
+ wrapped: Callable, instance: Any, args: List[Any], kwargs: Dict[str, Any]
86
+ ):
87
+ input_value = kwargs.get("input")
88
+ prompt = (
89
+ input_value[0]
90
+ if isinstance(input_value, list)
91
+ else [{"role": "user", "content": input_value}]
92
+ )
93
+ service_provider = SERVICE_PROVIDERS["OPENAI"]
94
+ span_attributes = {
95
+ "instructions": kwargs.get("instructions"),
96
+ **get_langtrace_attributes(version, service_provider, vendor_type="llm"),
97
+ **get_llm_request_attributes(
98
+ kwargs,
99
+ operation_name="openai.responses.create",
100
+ prompts=prompt,
101
+ ),
102
+ }
103
+ with tracer.start_as_current_span(
104
+ name="openai.responses.create",
105
+ kind=SpanKind.CLIENT,
106
+ context=set_span_in_context(trace.get_current_span()),
107
+ end_on_exit=False,
108
+ ) as span:
109
+ try:
110
+ set_span_attributes(span, span_attributes)
111
+
112
+ response = wrapped(*args, **kwargs)
113
+ if is_streaming(kwargs) and span.is_recording():
114
+ return StreamWrapper(response, span)
115
+ else:
116
+ _set_openai_agentic_response_attributes(span, response)
117
+ return response
118
+ except Exception as err:
119
+ span.record_exception(err)
120
+ raise
121
+
122
+ return traced_method
123
+
124
+
31
125
  def filter_valid_attributes(attributes):
32
126
  """Filter attributes where value is not None, not an empty string, and not openai.NOT_GIVEN."""
33
127
  return {
@@ -634,6 +728,21 @@ def extract_content(choice: Any) -> Union[str, List[Dict[str, Any]], Dict[str, A
634
728
  return ""
635
729
 
636
730
 
731
+ def _set_openai_agentic_response_attributes(span: Span, response) -> None:
732
+ set_span_attribute(span, SpanAttributes.LLM_RESPONSE_ID, response.id)
733
+ set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, response.model)
734
+ set_event_completion(span, [{"role": "assistant", "content": response.output_text}])
735
+ set_usage_attributes(
736
+ span,
737
+ {
738
+ "input_tokens": response.usage.input_tokens,
739
+ "output_tokens": response.usage.output_tokens,
740
+ "total_tokens": response.usage.total_tokens,
741
+ "cached_tokens": response.usage.input_tokens_details["cached_tokens"],
742
+ },
743
+ )
744
+
745
+
637
746
  @silently_fail
638
747
  def _set_input_attributes(
639
748
  span: Span, kwargs: ChatCompletionsCreateKwargs, attributes: LLMSpanAttributes
@@ -707,5 +816,9 @@ def _set_response_attributes(span: Span, result: ResultType) -> None:
707
816
  set_span_attribute(
708
817
  span,
709
818
  "gen_ai.usage.cached_tokens",
710
- result.usage.prompt_tokens_details.cached_tokens if result.usage.prompt_tokens_details else 0,
819
+ (
820
+ result.usage.prompt_tokens_details.cached_tokens
821
+ if result.usage.prompt_tokens_details
822
+ else 0
823
+ ),
711
824
  )
@@ -48,8 +48,8 @@ from langtrace_python_sdk.instrumentation import (
48
48
  GeminiInstrumentation, GoogleGenaiInstrumentation, GraphlitInstrumentation,
49
49
  GroqInstrumentation, LangchainCommunityInstrumentation,
50
50
  LangchainCoreInstrumentation, LangchainInstrumentation,
51
- LanggraphInstrumentation, LiteLLMInstrumentation,
52
- LlamaindexInstrumentation, MilvusInstrumentation, MistralInstrumentation,
51
+ LanggraphInstrumentation, LiteLLMInstrumentation, LlamaindexInstrumentation,
52
+ MilvusInstrumentation, MistralInstrumentation, Neo4jGraphRAGInstrumentation,
53
53
  OllamaInstrumentor, OpenAIAgentsInstrumentation, OpenAIInstrumentation,
54
54
  PhiDataInstrumentation, PineconeInstrumentation, PyMongoInstrumentation,
55
55
  QdrantInstrumentation, VertexAIInstrumentation, WeaviateInstrumentation)
@@ -284,6 +284,7 @@ def init(
284
284
  "phidata": PhiDataInstrumentation(),
285
285
  "agno": AgnoInstrumentation(),
286
286
  "mistralai": MistralInstrumentation(),
287
+ "neo4j-graphrag": Neo4jGraphRAGInstrumentation(),
287
288
  "boto3": AWSBedrockInstrumentation(),
288
289
  "autogen": AutogenInstrumentation(),
289
290
  "pymongo": PyMongoInstrumentation(),
@@ -96,22 +96,22 @@ def calculate_price_from_usage(model, usage):
96
96
 
97
97
  def convert_mistral_messages_to_serializable(mistral_messages):
98
98
  serializable_messages = []
99
-
99
+
100
100
  try:
101
101
  for message in mistral_messages:
102
102
  serializable_message = {"role": message.role}
103
-
103
+
104
104
  # Handle content
105
105
  if hasattr(message, "content"):
106
106
  serializable_message["content"] = message.content
107
-
107
+
108
108
  # Handle tool_calls
109
109
  if hasattr(message, "tool_calls") and message.tool_calls is not None:
110
110
  serializable_tool_calls = []
111
-
111
+
112
112
  for tool_call in message.tool_calls:
113
113
  serializable_tool_call = {}
114
-
114
+
115
115
  # Handle id, type, and index
116
116
  if hasattr(tool_call, "id"):
117
117
  serializable_tool_call["id"] = tool_call.id
@@ -119,111 +119,117 @@ def convert_mistral_messages_to_serializable(mistral_messages):
119
119
  serializable_tool_call["type"] = tool_call.type
120
120
  if hasattr(tool_call, "index"):
121
121
  serializable_tool_call["index"] = tool_call.index
122
-
122
+
123
123
  # Handle function
124
124
  if hasattr(tool_call, "function"):
125
125
  function_call = tool_call.function
126
126
  serializable_function = {}
127
-
127
+
128
128
  if hasattr(function_call, "name"):
129
129
  serializable_function["name"] = function_call.name
130
130
  if hasattr(function_call, "arguments"):
131
131
  serializable_function["arguments"] = function_call.arguments
132
-
132
+
133
133
  serializable_tool_call["function"] = serializable_function
134
-
134
+
135
135
  serializable_tool_calls.append(serializable_tool_call)
136
-
136
+
137
137
  serializable_message["tool_calls"] = serializable_tool_calls
138
-
138
+
139
139
  # Handle tool_call_id for tool messages
140
140
  if hasattr(message, "tool_call_id"):
141
141
  serializable_message["tool_call_id"] = message.tool_call_id
142
-
142
+
143
143
  serializable_messages.append(serializable_message)
144
144
  except Exception as e:
145
145
  pass
146
-
146
+
147
147
  return serializable_messages
148
148
 
149
149
 
150
150
  def convert_gemini_messages_to_serializable(formatted_messages, system_message=None):
151
151
  """
152
152
  Converts Gemini-formatted messages back to a JSON serializable format.
153
-
153
+
154
154
  Args:
155
155
  formatted_messages: The formatted messages from Gemini.
156
156
  system_message (str, optional): System message content.
157
-
157
+
158
158
  Returns:
159
159
  List[dict]: JSON serializable list of message dictionaries.
160
160
  """
161
161
  serializable_messages = []
162
-
162
+
163
163
  try:
164
164
  # Add system message if present
165
165
  if system_message:
166
- serializable_messages.append({
167
- "role": "system",
168
- "content": system_message
169
- })
170
-
166
+ serializable_messages.append({"role": "system", "content": system_message})
167
+
171
168
  for message_item in formatted_messages:
172
169
  # Handle the case where the item is a dict with 'role' and 'content' keys
173
- if isinstance(message_item, dict) and 'role' in message_item and 'content' in message_item:
174
- role = message_item['role']
175
- content_value = message_item['content']
176
-
170
+ if (
171
+ isinstance(message_item, dict)
172
+ and "role" in message_item
173
+ and "content" in message_item
174
+ ):
175
+ role = message_item["role"]
176
+ content_value = message_item["content"]
177
+
177
178
  # Initialize our serializable message
178
179
  serializable_message = {"role": role}
179
-
180
+
180
181
  # If content is a list of Content objects
181
182
  if isinstance(content_value, list) and len(content_value) > 0:
182
183
  for content_obj in content_value:
183
184
  # Process each Content object
184
- if hasattr(content_obj, 'parts') and hasattr(content_obj, 'role'):
185
+ if hasattr(content_obj, "parts") and hasattr(
186
+ content_obj, "role"
187
+ ):
185
188
  parts = content_obj.parts
186
-
189
+
187
190
  # Extract text from parts
188
191
  text_parts = []
189
192
  for part in parts:
190
- if hasattr(part, 'text') and part.text:
193
+ if hasattr(part, "text") and part.text:
191
194
  text_parts.append(part.text)
192
-
195
+
193
196
  if text_parts:
194
197
  serializable_message["content"] = " ".join(text_parts)
195
-
198
+
196
199
  # Here you can add additional processing for other part types
197
200
  # like function_call, function_response, inline_data, etc.
198
201
  # Similar to the previous implementation
199
-
202
+
200
203
  # If content is a string or already a primitive type
201
- elif isinstance(content_value, (str, int, float, bool)) or content_value is None:
204
+ elif (
205
+ isinstance(content_value, (str, int, float, bool))
206
+ or content_value is None
207
+ ):
202
208
  serializable_message["content"] = content_value
203
-
209
+
204
210
  # Add the processed message to our list
205
211
  serializable_messages.append(serializable_message)
206
-
212
+
207
213
  # Handle the case where the item is a Content object directly
208
- elif hasattr(message_item, 'role') and hasattr(message_item, 'parts'):
214
+ elif hasattr(message_item, "role") and hasattr(message_item, "parts"):
209
215
  # This is the case from the previous implementation
210
216
  # Process a Content object directly
211
217
  serializable_message = {"role": message_item.role}
212
-
218
+
213
219
  parts = message_item.parts
214
220
  text_parts = []
215
-
221
+
216
222
  for part in parts:
217
- if hasattr(part, 'text') and part.text:
223
+ if hasattr(part, "text") and part.text:
218
224
  text_parts.append(part.text)
219
-
225
+
220
226
  if text_parts:
221
227
  serializable_message["content"] = " ".join(text_parts)
222
-
228
+
223
229
  serializable_messages.append(serializable_message)
224
230
  except Exception as e:
225
231
  pass
226
-
232
+
227
233
  return serializable_messages
228
234
 
229
235
 
@@ -253,7 +259,7 @@ def get_llm_request_attributes(kwargs, prompts=None, model=None, operation_name=
253
259
  or kwargs.get("top_k", None)
254
260
  or kwargs.get("top_n", None)
255
261
  )
256
-
262
+
257
263
  try:
258
264
  prompts = json.dumps(prompts) if prompts else None
259
265
  except Exception as e:
@@ -261,9 +267,13 @@ def get_llm_request_attributes(kwargs, prompts=None, model=None, operation_name=
261
267
  # check model
262
268
  if kwargs.get("model") is not None:
263
269
  if kwargs.get("model").startswith("gemini"):
264
- prompts = json.dumps(convert_gemini_messages_to_serializable(prompts))
270
+ prompts = json.dumps(
271
+ convert_gemini_messages_to_serializable(prompts)
272
+ )
265
273
  elif kwargs.get("model").startswith("mistral"):
266
- prompts = json.dumps(convert_mistral_messages_to_serializable(prompts))
274
+ prompts = json.dumps(
275
+ convert_mistral_messages_to_serializable(prompts)
276
+ )
267
277
  else:
268
278
  prompts = "[]"
269
279
  else:
@@ -427,6 +437,7 @@ class StreamWrapper:
427
437
  "".join(self.result_content), response_model
428
438
  )
429
439
  if self._span_started:
440
+ print("SPAAN", self.span)
430
441
  set_span_attribute(
431
442
  self.span,
432
443
  SpanAttributes.LLM_RESPONSE_MODEL,
@@ -570,6 +581,9 @@ class StreamWrapper:
570
581
  and not hasattr(chunk.delta, "message")
571
582
  ):
572
583
  content = [chunk.delta.text] if hasattr(chunk.delta, "text") else []
584
+ # OpenAI Responses API
585
+ if hasattr(chunk, "type") and chunk.type == "response.completed":
586
+ content = [chunk.response.output_text]
573
587
 
574
588
  if isinstance(chunk, dict):
575
589
  if "message" in chunk:
@@ -579,7 +593,11 @@ class StreamWrapper:
579
593
  self.result_content.append(content[0])
580
594
 
581
595
  def set_usage_attributes(self, chunk):
582
-
596
+ # Responses API OpenAI
597
+ if hasattr(chunk, "type") and chunk.type == "response.completed":
598
+ usage = chunk.response.usage
599
+ self.completion_tokens = usage.output_tokens
600
+ self.prompt_tokens = usage.input_tokens
583
601
  # Anthropic & OpenAI
584
602
  if hasattr(chunk, "type") and chunk.type == "message_start":
585
603
  if hasattr(chunk.message, "usage") and chunk.message.usage is not None:
@@ -630,6 +648,7 @@ class StreamWrapper:
630
648
  and chunk.data.choices is not None
631
649
  ):
632
650
  chunk = chunk.data
651
+
633
652
  self.set_response_model(chunk=chunk)
634
653
  self.build_streaming_response(chunk=chunk)
635
654
  self.set_usage_attributes(chunk=chunk)
@@ -1 +1 @@
1
- __version__ = "3.8.6"
1
+ __version__ = "3.8.8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langtrace-python-sdk
3
- Version: 3.8.6
3
+ Version: 3.8.8
4
4
  Summary: Python SDK for LangTrace
5
5
  Project-URL: Homepage, https://github.com/Scale3-Labs/langtrace-python-sdk
6
6
  Author-email: Scale3 Labs <engineering@scale3labs.com>
@@ -115,8 +115,8 @@ examples/vertexai_example/main.py,sha256=gndId5X5ksD-ycxnAWMdEqIDbLc3kz5Vt8vm4YP
115
115
  examples/weaviate_example/__init__.py,sha256=8JMDBsRSEV10HfTd-YC7xb4txBjD3la56snk-Bbg2Kw,618
116
116
  examples/weaviate_example/query_text.py,sha256=wPHQTc_58kPoKTZMygVjTj-2ZcdrIuaausJfMxNQnQc,127162
117
117
  langtrace_python_sdk/__init__.py,sha256=VZM6i71NR7pBQK6XvJWRelknuTYUhqwqE7PlicKa5Wg,1166
118
- langtrace_python_sdk/langtrace.py,sha256=T-DsDrwWaL4gAUK1lkTRRpmvoO7F2WtO5hQZdyrVAxE,13791
119
- langtrace_python_sdk/version.py,sha256=L1_tSVoK2Iu6e_MbZl4Kogu3ops-t44WVGFD8F6BeAc,22
118
+ langtrace_python_sdk/langtrace.py,sha256=4nhcy9_T3h4m5T5Blyrvs0BJcKzPWMtkIyEDSbba8P8,13879
119
+ langtrace_python_sdk/version.py,sha256=3SjfTmtUQqjDdaQC3uiCoISksstrQ1Sqj4ASUtoUrzs,22
120
120
  langtrace_python_sdk/constants/__init__.py,sha256=3CNYkWMdd1DrkGqzLUgNZXjdAlM6UFMlf_F-odAToyc,146
121
121
  langtrace_python_sdk/constants/exporter/langtrace_exporter.py,sha256=EVCrouYCpY98f0KSaKr4PzNxPULTZZO6dSA_crEOyJU,106
122
122
  langtrace_python_sdk/constants/instrumentation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -141,7 +141,7 @@ langtrace_python_sdk/constants/instrumentation/weaviate.py,sha256=gtv-JBxvNGClEM
141
141
  langtrace_python_sdk/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
142
142
  langtrace_python_sdk/extensions/langtrace_exporter.py,sha256=ckd8dMmY6h2oxE04p1JFLwUB5PSJX_Cy4eDFEM6aj4Y,6605
143
143
  langtrace_python_sdk/extensions/langtrace_filesystem.py,sha256=34fZutG28EJ66l67OvTGsydAH3ZpXgikdE7hVLqBpG4,7863
144
- langtrace_python_sdk/instrumentation/__init__.py,sha256=DC96oELorZedDf5zooQ6HGi-dmieXwUephgeB_LzfaU,2559
144
+ langtrace_python_sdk/instrumentation/__init__.py,sha256=lw53EsTTtOK-dMhImudipmbVhZqORylt4hO5A7Mv8Uo,2652
145
145
  langtrace_python_sdk/instrumentation/agno/__init__.py,sha256=95fn4oA-CHB0mxc6KnVB20KSbXGl_ZZr9n99EEaXzrY,91
146
146
  langtrace_python_sdk/instrumentation/agno/instrumentation.py,sha256=XUnfvqpp13IgdF03xGKasq7kGjeaN1sXLIwCf-Nt_Nc,2667
147
147
  langtrace_python_sdk/instrumentation/agno/patch.py,sha256=qCUxCkzU9cYu_d8BzLgj_Ss97qib07tRVYpYDiNnNMs,16876
@@ -218,12 +218,15 @@ langtrace_python_sdk/instrumentation/milvus/patch.py,sha256=0yY5aQz0x7hpQZ8U-0qf
218
218
  langtrace_python_sdk/instrumentation/mistral/__init__.py,sha256=mkGALBQvq0jSfwDl6TU09SFwnVs6O4zkUi-yVmd3SNg,90
219
219
  langtrace_python_sdk/instrumentation/mistral/instrumentation.py,sha256=qtCkHCSOaiicUChbmTID4lcK1rbeW8oRSbpda2ogbgM,2328
220
220
  langtrace_python_sdk/instrumentation/mistral/patch.py,sha256=5EzqMeIEcMUH8P-l9-ijpRNWSc6mYwe9Gz0VY4PLrS0,6559
221
+ langtrace_python_sdk/instrumentation/neo4j_graphrag/__init__.py,sha256=K-Qpfh-kn2EPxnYZ_oubLZNqAUO9Cx7mnMrJu_ms3UU,102
222
+ langtrace_python_sdk/instrumentation/neo4j_graphrag/instrumentation.py,sha256=FLnddPSozuu5puqYD_FnXLMFet8YToVdWP-cY5QZrvM,2216
223
+ langtrace_python_sdk/instrumentation/neo4j_graphrag/patch.py,sha256=4c7NKcTJeYsbi5V4aSsuPzTiFoBqtlcXmYx2bMlE7qg,9713
221
224
  langtrace_python_sdk/instrumentation/ollama/__init__.py,sha256=g2zJsXnDHinXPzTc-WxDeTtHmr9gmAj3K6l_00kP8c8,82
222
225
  langtrace_python_sdk/instrumentation/ollama/instrumentation.py,sha256=jdsvkqUJAAUNLVPtAkn_rG26HXetVQXWtjn4a6eWZro,2029
223
226
  langtrace_python_sdk/instrumentation/ollama/patch.py,sha256=w99r9wCCVDdJnZQEezYE2lW_iNFEtrldt9vq3ISAsag,5375
224
227
  langtrace_python_sdk/instrumentation/openai/__init__.py,sha256=VPHRNCQEdkizIVP2d0Uw_a7t8XOTSTprEIB8oboJFbs,95
225
- langtrace_python_sdk/instrumentation/openai/instrumentation.py,sha256=PZxI0qfoud1VsKGmJu49YDp0Z9z9TzCR8qxR3uznOMA,2810
226
- langtrace_python_sdk/instrumentation/openai/patch.py,sha256=mRE150sHGL2dq4oOKTYWek1vup7HuRW_sqP6nNOd3N8,27862
228
+ langtrace_python_sdk/instrumentation/openai/instrumentation.py,sha256=kvbRr5DrcP23ka4mLZpwr7W0ZxeTGHilkOlc_h5V5s8,3226
229
+ langtrace_python_sdk/instrumentation/openai/patch.py,sha256=I_Gzp8ULbOQzlG9PltrembOiBvfp0FQHDRwR5q5tmZg,31358
227
230
  langtrace_python_sdk/instrumentation/openai/types.py,sha256=aVkoa7tmAbYfyOhnyMrDaVjQuwhmRNLMthlNtKMtWX8,4311
228
231
  langtrace_python_sdk/instrumentation/openai_agents/__init__.py,sha256=ElRfFIQYXD2-eRyL3fZnjIsDJLTrDolh5cZHPnZv0q8,107
229
232
  langtrace_python_sdk/instrumentation/openai_agents/instrumentation.py,sha256=6M4FHXfem7pazrNgsimebrEfMb2FxI8lHrdEMbVf75Y,1860
@@ -249,7 +252,7 @@ langtrace_python_sdk/instrumentation/weaviate/patch.py,sha256=Lqixz32uAvDA2VLU3z
249
252
  langtrace_python_sdk/types/__init__.py,sha256=SJSJzkgPjGGTVJXUZ_FyR3p9DJ5kWGx7iAnJfY4ZYHU,4669
250
253
  langtrace_python_sdk/utils/__init__.py,sha256=VVDOG-QLd59ZvSHp0avjof0sbxlZ1QQOf0KoOF7ofhQ,3310
251
254
  langtrace_python_sdk/utils/langtrace_sampler.py,sha256=BupNndHbU9IL_wGleKetz8FdcveqHMBVz1bfKTTW80w,1753
252
- langtrace_python_sdk/utils/llm.py,sha256=giJU33LvMPaRjPAjUwBCehgHj_ei1HwM7gLJSVWYLnI,23238
255
+ langtrace_python_sdk/utils/llm.py,sha256=UGfCfOcQ6NiGKsA1_aJt9Pt7yi4iGKq26TJQroOwktg,23463
253
256
  langtrace_python_sdk/utils/misc.py,sha256=LaQr5LOmZMiuwVdjYh7aIu6o2C_Xb1wgpQGNOVmRzfE,1918
254
257
  langtrace_python_sdk/utils/prompt_registry.py,sha256=n5dQMVLBw8aJZY8Utvf67bncc25ELf6AH9BYw8_hSzo,2619
255
258
  langtrace_python_sdk/utils/sdk_version_checker.py,sha256=F-VVVH7Fmhr5LcY0IIe-34zIi5RQcx26uuxFpPzZesM,1782
@@ -300,8 +303,8 @@ tests/pinecone/cassettes/test_query.yaml,sha256=b5v9G3ssUy00oG63PlFUR3JErF2Js-5A
300
303
  tests/pinecone/cassettes/test_upsert.yaml,sha256=neWmQ1v3d03V8WoLl8FoFeeCYImb8pxlJBWnFd_lITU,38607
301
304
  tests/qdrant/conftest.py,sha256=9n0uHxxIjWk9fbYc4bx-uP8lSAgLBVx-cV9UjnsyCHM,381
302
305
  tests/qdrant/test_qdrant.py,sha256=pzjAjVY2kmsmGfrI2Gs2xrolfuaNHz7l1fqGQCjp5_o,3353
303
- langtrace_python_sdk-3.8.6.dist-info/METADATA,sha256=4oUaxQYAGDXx_tRBdNnMja_qs0XOH-KOTYSwWizKc4g,15844
304
- langtrace_python_sdk-3.8.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
305
- langtrace_python_sdk-3.8.6.dist-info/entry_points.txt,sha256=1_b9-qvf2fE7uQNZcbUei9vLpFZBbbh9LrtGw95ssAo,70
306
- langtrace_python_sdk-3.8.6.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
307
- langtrace_python_sdk-3.8.6.dist-info/RECORD,,
306
+ langtrace_python_sdk-3.8.8.dist-info/METADATA,sha256=deElB8UvrgQqY6ZaOFhY7bKQnvXEinTTrnNkgz4b04o,15844
307
+ langtrace_python_sdk-3.8.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
308
+ langtrace_python_sdk-3.8.8.dist-info/entry_points.txt,sha256=1_b9-qvf2fE7uQNZcbUei9vLpFZBbbh9LrtGw95ssAo,70
309
+ langtrace_python_sdk-3.8.8.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
310
+ langtrace_python_sdk-3.8.8.dist-info/RECORD,,