Skip to content

Commit 3db662b

Browse files
committed
Merge branch 'development' into release-3.8.8
2 parents 48ccbf5 + eddcb2e commit 3db662b

File tree

6 files changed

+300
-3
lines changed

6 files changed

+300
-3
lines changed

src/langtrace_python_sdk/instrumentation/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from .llamaindex import LlamaindexInstrumentation
2323
from .milvus import MilvusInstrumentation
2424
from .mistral import MistralInstrumentation
25+
from .neo4j_graphrag import Neo4jGraphRAGInstrumentation
2526
from .ollama import OllamaInstrumentor
2627
from .openai import OpenAIInstrumentation
2728
from .openai_agents import OpenAIAgentsInstrumentation
@@ -59,6 +60,7 @@
5960
"AWSBedrockInstrumentation",
6061
"CerebrasInstrumentation",
6162
"MilvusInstrumentation",
63+
"Neo4jGraphRAGInstrumentation",
6264
"GoogleGenaiInstrumentation",
6365
"CrewaiToolsInstrumentation",
6466
"GraphlitInstrumentation",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .instrumentation import Neo4jGraphRAGInstrumentation
2+
3+
__all__ = ["Neo4jGraphRAGInstrumentation"]
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

src/langtrace_python_sdk/langtrace.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@
4848
GeminiInstrumentation, GoogleGenaiInstrumentation, GraphlitInstrumentation,
4949
GroqInstrumentation, LangchainCommunityInstrumentation,
5050
LangchainCoreInstrumentation, LangchainInstrumentation,
51-
LanggraphInstrumentation, LiteLLMInstrumentation,
52-
LlamaindexInstrumentation, MilvusInstrumentation, MistralInstrumentation,
51+
LanggraphInstrumentation, LiteLLMInstrumentation, LlamaindexInstrumentation,
52+
MilvusInstrumentation, MistralInstrumentation, Neo4jGraphRAGInstrumentation,
5353
OllamaInstrumentor, OpenAIAgentsInstrumentation, OpenAIInstrumentation,
5454
PhiDataInstrumentation, PineconeInstrumentation, PyMongoInstrumentation,
5555
QdrantInstrumentation, VertexAIInstrumentation, WeaviateInstrumentation)
@@ -284,6 +284,7 @@ def init(
284284
"phidata": PhiDataInstrumentation(),
285285
"agno": AgnoInstrumentation(),
286286
"mistralai": MistralInstrumentation(),
287+
"neo4j-graphrag": Neo4jGraphRAGInstrumentation(),
287288
"boto3": AWSBedrockInstrumentation(),
288289
"autogen": AutogenInstrumentation(),
289290
"pymongo": PyMongoInstrumentation(),
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "3.8.7"
1+
__version__ = "3.8.8"

0 commit comments

Comments
 (0)