From 2349adc102716a77a22df6df88a4b306fabc9801 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Thu, 20 Mar 2025 13:22:55 +0300 Subject: [PATCH 1/9] add instrumentation for neo4j --- .../constants/instrumentation/neo4j.py | 36 ++++++ .../instrumentation/__init__.py | 1 + .../instrumentation/neo4j/__init__.py | 3 + .../instrumentation/neo4j/instrumentation.py | 55 +++++++++ .../instrumentation/neo4j/patch.py | 111 ++++++++++++++++++ src/langtrace_python_sdk/langtrace.py | 7 +- 6 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 src/langtrace_python_sdk/constants/instrumentation/neo4j.py create mode 100644 src/langtrace_python_sdk/instrumentation/neo4j/__init__.py create mode 100644 src/langtrace_python_sdk/instrumentation/neo4j/instrumentation.py create mode 100644 src/langtrace_python_sdk/instrumentation/neo4j/patch.py diff --git a/src/langtrace_python_sdk/constants/instrumentation/neo4j.py b/src/langtrace_python_sdk/constants/instrumentation/neo4j.py new file mode 100644 index 00000000..a53b45f6 --- /dev/null +++ b/src/langtrace_python_sdk/constants/instrumentation/neo4j.py @@ -0,0 +1,36 @@ +from langtrace.trace_attributes import Neo4jMethods + +APIS = { + "RUN": { + "METHOD": Neo4jMethods.RUN.value, + "OPERATION": "run", + }, + "BEGIN_TRANSACTION": { + "METHOD": Neo4jMethods.BEGIN_TRANSACTION.value, + "OPERATION": "begin_transaction", + }, + "READ_TRANSACTION": { + "METHOD": Neo4jMethods.READ_TRANSACTION.value, + "OPERATION": "read_transaction", + }, + "WRITE_TRANSACTION": { + "METHOD": Neo4jMethods.WRITE_TRANSACTION.value, + "OPERATION": "write_transaction", + }, + "EXECUTE_READ": { + "METHOD": Neo4jMethods.EXECUTE_READ.value, + "OPERATION": "execute_read", + }, + "EXECUTE_WRITE": { + "METHOD": Neo4jMethods.EXECUTE_WRITE.value, + "OPERATION": "execute_write", + }, + "EXECUTE_QUERY": { + "METHOD": Neo4jMethods.EXECUTE_QUERY.value, + "OPERATION": "execute_query", + }, + "TX_RUN": { + "METHOD": Neo4jMethods.TX_RUN.value, + "OPERATION": "tx_run", + }, +} \ No newline at end of file diff --git a/src/langtrace_python_sdk/instrumentation/__init__.py b/src/langtrace_python_sdk/instrumentation/__init__.py index c49a7800..e26f13a1 100644 --- a/src/langtrace_python_sdk/instrumentation/__init__.py +++ b/src/langtrace_python_sdk/instrumentation/__init__.py @@ -22,6 +22,7 @@ from .llamaindex import LlamaindexInstrumentation from .milvus import MilvusInstrumentation from .mistral import MistralInstrumentation +from .neo4j import Neo4jInstrumentation from .neo4j_graphrag import Neo4jGraphRAGInstrumentation from .ollama import OllamaInstrumentor from .openai import OpenAIInstrumentation diff --git a/src/langtrace_python_sdk/instrumentation/neo4j/__init__.py b/src/langtrace_python_sdk/instrumentation/neo4j/__init__.py new file mode 100644 index 00000000..18e1e63a --- /dev/null +++ b/src/langtrace_python_sdk/instrumentation/neo4j/__init__.py @@ -0,0 +1,3 @@ +from .instrumentation import Neo4jInstrumentation + +__all__ = ["Neo4jInstrumentation"] diff --git a/src/langtrace_python_sdk/instrumentation/neo4j/instrumentation.py b/src/langtrace_python_sdk/instrumentation/neo4j/instrumentation.py new file mode 100644 index 00000000..605ec5ed --- /dev/null +++ b/src/langtrace_python_sdk/instrumentation/neo4j/instrumentation.py @@ -0,0 +1,55 @@ +""" +Copyright (c) 2025 Scale3 Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import importlib.metadata +import logging +from typing import Collection + +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.trace import get_tracer +from wrapt import wrap_function_wrapper + +from langtrace_python_sdk.constants.instrumentation.neo4j import APIS +from langtrace_python_sdk.instrumentation.neo4j.patch import ( + session_patch, + driver_patch, + transaction_patch +) + +logging.basicConfig(level=logging.FATAL) + + +class Neo4jInstrumentation(BaseInstrumentor): + """ + The Neo4jInstrumentation class represents the Neo4j graph database instrumentation + """ + + def instrumentation_dependencies(self) -> Collection[str]: + return ["neo4j >= 5.25.0"] + + def _instrument(self, **kwargs): + tracer_provider = kwargs.get("tracer_provider") + tracer = get_tracer(__name__, "", tracer_provider) + version = importlib.metadata.version("neo4j") + + wrap_function_wrapper( + "neo4j._sync.driver", + "Driver.execute_query", + driver_patch("EXECUTE_QUERY", version, tracer), + ) + + def _uninstrument(self, **kwargs): + pass \ No newline at end of file diff --git a/src/langtrace_python_sdk/instrumentation/neo4j/patch.py b/src/langtrace_python_sdk/instrumentation/neo4j/patch.py new file mode 100644 index 00000000..a3bd9ca2 --- /dev/null +++ b/src/langtrace_python_sdk/instrumentation/neo4j/patch.py @@ -0,0 +1,111 @@ +""" +Copyright (c) 2025 Scale3 Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import json + +from langtrace_python_sdk.utils.llm import get_span_name +from langtrace_python_sdk.utils.silently_fail import silently_fail +from langtrace.trace_attributes import DatabaseSpanAttributes +from langtrace_python_sdk.utils import set_span_attribute +from opentelemetry import baggage, trace +from opentelemetry.trace import SpanKind +from opentelemetry.trace.status import Status, StatusCode +from opentelemetry.trace.propagation import set_span_in_context + +from langtrace_python_sdk.constants.instrumentation.common import ( + LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY, + SERVICE_PROVIDERS, +) +from langtrace_python_sdk.constants.instrumentation.neo4j import APIS +from importlib.metadata import version as v + +from langtrace_python_sdk.constants import LANGTRACE_SDK_NAME + + +def driver_patch(operation_name, version, tracer): + def traced_method(wrapped, instance, args, kwargs): + + api = APIS[operation_name] + service_provider = SERVICE_PROVIDERS.get("NEO4J", "neo4j") + extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY) + + span_attributes = { + "langtrace.sdk.name": "langtrace-python-sdk", + "langtrace.service.name": service_provider, + "langtrace.service.type": "vectordb", + "langtrace.service.version": version, + "langtrace.version": v(LANGTRACE_SDK_NAME), + "db.system": "neo4j", + "db.operation": api["OPERATION"], + "db.query": json.dumps(kwargs), + **(extra_attributes if extra_attributes is not None else {}), + } + + attributes = DatabaseSpanAttributes(**span_attributes) + + with tracer.start_as_current_span( + name=get_span_name(api["METHOD"]), + kind=SpanKind.CLIENT, + context=set_span_in_context(trace.get_current_span()), + ) as span: + for field, value in attributes.model_dump(by_alias=True).items(): + if value is not None: + span.set_attribute(field, value) + + if operation_name == "EXECUTE_QUERY": + _set_execute_query_attributes(span, args, kwargs) + + try: + result = wrapped(*args, **kwargs) + span.set_status(StatusCode.OK) + return result + except Exception as err: + span.record_exception(err) + span.set_status(Status(StatusCode.ERROR, str(err))) + raise + + return traced_method + + +@silently_fail +def _set_execute_query_attributes(span, args, kwargs): + query = args[0] if args else kwargs.get("query_", None) + if query: + if hasattr(query, "text"): + set_span_attribute(span, "db.statement", query.text) + set_span_attribute(span, "db.query", query.text) + if hasattr(query, "metadata") and query.metadata: + set_span_attribute(span, "db.query.metadata", json.dumps(query.metadata)) + if hasattr(query, "timeout") and query.timeout: + set_span_attribute(span, "db.query.timeout", query.timeout) + else: + set_span_attribute(span, "db.statement", query) + set_span_attribute(span, "db.query", query) + + parameters = kwargs.get("parameters_", None) + if parameters: + try: + set_span_attribute(span, "db.statement.parameters", json.dumps(parameters)) + except (TypeError, ValueError): + pass + + database = kwargs.get("database_", None) + if database: + set_span_attribute(span, "db.name", database) + + routing = kwargs.get("routing_", None) + if routing: + set_span_attribute(span, "db.routing", str(routing)) \ No newline at end of file diff --git a/src/langtrace_python_sdk/langtrace.py b/src/langtrace_python_sdk/langtrace.py index 6f16f998..d01aeae4 100644 --- a/src/langtrace_python_sdk/langtrace.py +++ b/src/langtrace_python_sdk/langtrace.py @@ -47,9 +47,9 @@ CrewaiToolsInstrumentation, DspyInstrumentation, EmbedchainInstrumentation, GeminiInstrumentation, GoogleGenaiInstrumentation, GraphlitInstrumentation, GroqInstrumentation, LangchainCommunityInstrumentation, - LangchainCoreInstrumentation, LangchainInstrumentation, - LanggraphInstrumentation, LiteLLMInstrumentation, LlamaindexInstrumentation, - MilvusInstrumentation, MistralInstrumentation, Neo4jGraphRAGInstrumentation, + LangchainCoreInstrumentation, LangchainInstrumentation, LanggraphInstrumentation, + LiteLLMInstrumentation, LlamaindexInstrumentation, MilvusInstrumentation, + MistralInstrumentation, Neo4jInstrumentation, Neo4jGraphRAGInstrumentation, OllamaInstrumentor, OpenAIAgentsInstrumentation, OpenAIInstrumentation, PhiDataInstrumentation, PineconeInstrumentation, PyMongoInstrumentation, QdrantInstrumentation, VertexAIInstrumentation, WeaviateInstrumentation) @@ -284,6 +284,7 @@ def init( "phidata": PhiDataInstrumentation(), "agno": AgnoInstrumentation(), "mistralai": MistralInstrumentation(), + "neo4j": Neo4jInstrumentation(), "neo4j-graphrag": Neo4jGraphRAGInstrumentation(), "boto3": AWSBedrockInstrumentation(), "autogen": AutogenInstrumentation(), From 98263648a65551d138ac1712c5ca2ddde2071b10 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Thu, 20 Mar 2025 13:23:53 +0300 Subject: [PATCH 2/9] update version --- src/langtrace_python_sdk/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/langtrace_python_sdk/version.py b/src/langtrace_python_sdk/version.py index a48af93e..42f8b2e3 100644 --- a/src/langtrace_python_sdk/version.py +++ b/src/langtrace_python_sdk/version.py @@ -1 +1 @@ -__version__ = "3.8.8" +__version__ = "3.8.9" From ea6bcd730f9b4630bae69be051f5fd89a68afd85 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Thu, 20 Mar 2025 15:37:01 +0300 Subject: [PATCH 3/9] update trace attributes version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index de51131c..43712260 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ classifiers = [ "Operating System :: OS Independent", ] dependencies = [ - 'trace-attributes==7.2.0', + 'trace-attributes==7.2.1', 'opentelemetry-api>=1.25.0', 'opentelemetry-sdk>=1.25.0', 'opentelemetry-instrumentation>=0.47b0', From 6379505826570f5d6fc186e953e1b0465b706ce9 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Mon, 24 Mar 2025 15:06:09 +0300 Subject: [PATCH 4/9] update imports --- .../instrumentation/neo4j/instrumentation.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/langtrace_python_sdk/instrumentation/neo4j/instrumentation.py b/src/langtrace_python_sdk/instrumentation/neo4j/instrumentation.py index 605ec5ed..58b936bc 100644 --- a/src/langtrace_python_sdk/instrumentation/neo4j/instrumentation.py +++ b/src/langtrace_python_sdk/instrumentation/neo4j/instrumentation.py @@ -23,11 +23,7 @@ from wrapt import wrap_function_wrapper from langtrace_python_sdk.constants.instrumentation.neo4j import APIS -from langtrace_python_sdk.instrumentation.neo4j.patch import ( - session_patch, - driver_patch, - transaction_patch -) +from langtrace_python_sdk.instrumentation.neo4j.patch import driver_patch logging.basicConfig(level=logging.FATAL) From ed83a02eed654f6bff0885b6de8e56a7a14570b6 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Wed, 26 Mar 2025 14:54:02 +0300 Subject: [PATCH 5/9] add neo4j result span attributes --- .../instrumentation/neo4j/patch.py | 91 +++++++++++++++++-- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/src/langtrace_python_sdk/instrumentation/neo4j/patch.py b/src/langtrace_python_sdk/instrumentation/neo4j/patch.py index a3bd9ca2..ce1e487d 100644 --- a/src/langtrace_python_sdk/instrumentation/neo4j/patch.py +++ b/src/langtrace_python_sdk/instrumentation/neo4j/patch.py @@ -48,9 +48,9 @@ def traced_method(wrapped, instance, args, kwargs): "langtrace.service.type": "vectordb", "langtrace.service.version": version, "langtrace.version": v(LANGTRACE_SDK_NAME), - "db.system": "neo4j", - "db.operation": api["OPERATION"], - "db.query": json.dumps(kwargs), + "neo4j.db.system": "neo4j", + "neo4j.db.operation": api["OPERATION"], + "neo4j.db.query": json.dumps(kwargs), **(extra_attributes if extra_attributes is not None else {}), } @@ -70,6 +70,11 @@ def traced_method(wrapped, instance, args, kwargs): try: result = wrapped(*args, **kwargs) + + if isinstance(result, tuple) and len(result) == 3: + records, result_summary, keys = result + _set_result_attributes(span, records, result_summary, keys) + span.set_status(StatusCode.OK) return result except Exception as err: @@ -85,14 +90,12 @@ def _set_execute_query_attributes(span, args, kwargs): query = args[0] if args else kwargs.get("query_", None) if query: if hasattr(query, "text"): - set_span_attribute(span, "db.statement", query.text) set_span_attribute(span, "db.query", query.text) if hasattr(query, "metadata") and query.metadata: set_span_attribute(span, "db.query.metadata", json.dumps(query.metadata)) if hasattr(query, "timeout") and query.timeout: set_span_attribute(span, "db.query.timeout", query.timeout) else: - set_span_attribute(span, "db.statement", query) set_span_attribute(span, "db.query", query) parameters = kwargs.get("parameters_", None) @@ -104,8 +107,82 @@ def _set_execute_query_attributes(span, args, kwargs): database = kwargs.get("database_", None) if database: - set_span_attribute(span, "db.name", database) + set_span_attribute(span, "neo4j.db.name", database) routing = kwargs.get("routing_", None) if routing: - set_span_attribute(span, "db.routing", str(routing)) \ No newline at end of file + set_span_attribute(span, "neo4j.db.routing", str(routing)) + + +@silently_fail +def _set_result_attributes(span, records, result_summary, keys): + """ + Set attributes related to the query result and summary + """ + # Set record count + if records is not None: + record_count = len(records) + set_span_attribute(span, "neo4j.result.record_count", record_count) + if record_count > 0: + set_span_attribute(span, "neo4j.result.records", json.dumps(records)) + + # Set keys information + if keys is not None: + set_span_attribute(span, "neo4j.result.keys", json.dumps(keys)) + + # Process result summary if available + if result_summary: + # Database info + if hasattr(result_summary, "database") and result_summary.database: + set_span_attribute(span, "neo4j.db.name", result_summary.database) + + # Query type + if hasattr(result_summary, "query_type") and result_summary.query_type: + set_span_attribute(span, "neo4j.result.query_type", result_summary.query_type) + + # Parameters + if hasattr(result_summary, "parameters") and result_summary.parameters: + try: + set_span_attribute(span, "neo4j.result.parameters", json.dumps(result_summary.parameters)) + except (TypeError, ValueError): + pass + + # Timing information + if hasattr(result_summary, "result_available_after") and result_summary.result_available_after is not None: + set_span_attribute(span, "neo4j.result.available_after_ms", result_summary.result_available_after) + + if hasattr(result_summary, "result_consumed_after") and result_summary.result_consumed_after is not None: + set_span_attribute(span, "neo4j.result.consumed_after_ms", result_summary.result_consumed_after) + + # Process counters + if hasattr(result_summary, "counters") and result_summary.counters: + counters = result_summary.counters + if hasattr(counters, "nodes_created") and counters.nodes_created: + set_span_attribute(span, "neo4j.result.nodes_created", counters.nodes_created) + + if hasattr(counters, "nodes_deleted") and counters.nodes_deleted: + set_span_attribute(span, "neo4j.result.nodes_deleted", counters.nodes_deleted) + + if hasattr(counters, "relationships_created") and counters.relationships_created: + set_span_attribute(span, "neo4j.result.relationships_created", counters.relationships_created) + + if hasattr(counters, "relationships_deleted") and counters.relationships_deleted: + set_span_attribute(span, "neo4j.result.relationships_deleted", counters.relationships_deleted) + + if hasattr(counters, "properties_set") and counters.properties_set: + set_span_attribute(span, "neo4j.result.properties_set", counters.properties_set) + + # Process plan/profile if available + if hasattr(result_summary, "plan") and result_summary.plan: + try: + set_span_attribute(span, "neo4j.result.plan", json.dumps(result_summary.plan)) + except (TypeError, ValueError): + pass + + # Process notifications + if hasattr(result_summary, "notifications") and result_summary.notifications: + try: + set_span_attribute(span, "neo4j.result.notification_count", len(result_summary.notifications)) + set_span_attribute(span, "neo4j.result.notifications", json.dumps(result_summary.notifications)) + except (AttributeError, TypeError): + pass \ No newline at end of file From a3c4c2e4d52e25e8e1c0f6a42ffb7c738f3c81d4 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Wed, 26 Mar 2025 14:56:27 +0300 Subject: [PATCH 6/9] add neo4j result span attributes --- .../instrumentation/neo4j/patch.py | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/langtrace_python_sdk/instrumentation/neo4j/patch.py b/src/langtrace_python_sdk/instrumentation/neo4j/patch.py index ce1e487d..0fcde574 100644 --- a/src/langtrace_python_sdk/instrumentation/neo4j/patch.py +++ b/src/langtrace_python_sdk/instrumentation/neo4j/patch.py @@ -119,42 +119,34 @@ def _set_result_attributes(span, records, result_summary, keys): """ Set attributes related to the query result and summary """ - # Set record count if records is not None: record_count = len(records) set_span_attribute(span, "neo4j.result.record_count", record_count) if record_count > 0: set_span_attribute(span, "neo4j.result.records", json.dumps(records)) - - # Set keys information + if keys is not None: set_span_attribute(span, "neo4j.result.keys", json.dumps(keys)) - - # Process result summary if available + if result_summary: - # Database info if hasattr(result_summary, "database") and result_summary.database: set_span_attribute(span, "neo4j.db.name", result_summary.database) - - # Query type + if hasattr(result_summary, "query_type") and result_summary.query_type: set_span_attribute(span, "neo4j.result.query_type", result_summary.query_type) - - # Parameters + if hasattr(result_summary, "parameters") and result_summary.parameters: try: set_span_attribute(span, "neo4j.result.parameters", json.dumps(result_summary.parameters)) except (TypeError, ValueError): pass - # Timing information if hasattr(result_summary, "result_available_after") and result_summary.result_available_after is not None: set_span_attribute(span, "neo4j.result.available_after_ms", result_summary.result_available_after) if hasattr(result_summary, "result_consumed_after") and result_summary.result_consumed_after is not None: set_span_attribute(span, "neo4j.result.consumed_after_ms", result_summary.result_consumed_after) - - # Process counters + if hasattr(result_summary, "counters") and result_summary.counters: counters = result_summary.counters if hasattr(counters, "nodes_created") and counters.nodes_created: @@ -171,15 +163,13 @@ def _set_result_attributes(span, records, result_summary, keys): if hasattr(counters, "properties_set") and counters.properties_set: set_span_attribute(span, "neo4j.result.properties_set", counters.properties_set) - - # Process plan/profile if available + if hasattr(result_summary, "plan") and result_summary.plan: try: set_span_attribute(span, "neo4j.result.plan", json.dumps(result_summary.plan)) except (TypeError, ValueError): pass - - # Process notifications + if hasattr(result_summary, "notifications") and result_summary.notifications: try: set_span_attribute(span, "neo4j.result.notification_count", len(result_summary.notifications)) From 081e457a2ce24a8aae88fc52095e6b2fd48bf5c9 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Wed, 26 Mar 2025 15:00:49 +0300 Subject: [PATCH 7/9] bump bersion --- src/langtrace_python_sdk/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/langtrace_python_sdk/version.py b/src/langtrace_python_sdk/version.py index 42f8b2e3..01d6f588 100644 --- a/src/langtrace_python_sdk/version.py +++ b/src/langtrace_python_sdk/version.py @@ -1 +1 @@ -__version__ = "3.8.9" +__version__ = "3.8.10" From 565f3b912c8adc210a3d1000a800dd0aad1b517b Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Wed, 26 Mar 2025 19:55:53 +0300 Subject: [PATCH 8/9] fix db attributes error and add examples --- src/examples/neo4j_example/__init__.py | 8 +++ src/examples/neo4j_example/basic.py | 26 +++++++++ .../neo4j_graphrag_example/__init__.py | 9 +++ src/examples/neo4j_graphrag_example/basic.py | 52 ++++++++++++++++++ .../neo4j_graphrag_example/data/abramov.pdf | Bin 0 -> 46933 bytes .../instrumentation/neo4j/patch.py | 6 +- src/langtrace_python_sdk/version.py | 2 +- src/run_example.py | 16 +++++- 8 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 src/examples/neo4j_example/__init__.py create mode 100644 src/examples/neo4j_example/basic.py create mode 100644 src/examples/neo4j_graphrag_example/__init__.py create mode 100644 src/examples/neo4j_graphrag_example/basic.py create mode 100644 src/examples/neo4j_graphrag_example/data/abramov.pdf diff --git a/src/examples/neo4j_example/__init__.py b/src/examples/neo4j_example/__init__.py new file mode 100644 index 00000000..3a1740c9 --- /dev/null +++ b/src/examples/neo4j_example/__init__.py @@ -0,0 +1,8 @@ +from .basic import execute_query +from langtrace_python_sdk import with_langtrace_root_span + + +class Neo4jRunner: + @with_langtrace_root_span("Neo4jRunner") + def run(self): + execute_query() diff --git a/src/examples/neo4j_example/basic.py b/src/examples/neo4j_example/basic.py new file mode 100644 index 00000000..12c56c44 --- /dev/null +++ b/src/examples/neo4j_example/basic.py @@ -0,0 +1,26 @@ +import os +from langtrace_python_sdk import langtrace +from neo4j import GraphDatabase + +langtrace.init() + +def execute_query(): + driver = GraphDatabase.driver( + os.getenv("NEO4J_URI"), + auth=(os.getenv("NEO4J_USERNAME"), os.getenv("NEO4J_PASSWORD")) + ) + + records, summary, keys = driver.execute_query( + "MATCH (p:Person {age: $age}) RETURN p.name AS name", + age=42, + database_=os.getenv("NEO4J_DATABASE"), + ) + + # Loop through results and do something with them + for person in records: + print(person) + # Summary information + print("The query `{query}` returned {records_count} records in {time} ms.".format( + query=summary.query, records_count=len(records), + time=summary.result_available_after, + )) diff --git a/src/examples/neo4j_graphrag_example/__init__.py b/src/examples/neo4j_graphrag_example/__init__.py new file mode 100644 index 00000000..198c13c5 --- /dev/null +++ b/src/examples/neo4j_graphrag_example/__init__.py @@ -0,0 +1,9 @@ +import asyncio +from .basic import search +from langtrace_python_sdk import with_langtrace_root_span + + +class Neo4jGraphRagRunner: + @with_langtrace_root_span("Neo4jGraphRagRunner") + def run(self): + asyncio.run(search()) diff --git a/src/examples/neo4j_graphrag_example/basic.py b/src/examples/neo4j_graphrag_example/basic.py new file mode 100644 index 00000000..79c4fa7b --- /dev/null +++ b/src/examples/neo4j_graphrag_example/basic.py @@ -0,0 +1,52 @@ +import os +from langtrace_python_sdk import langtrace +from langtrace_python_sdk.utils.with_root_span import with_langtrace_root_span +from neo4j import GraphDatabase +from neo4j_graphrag.generation import GraphRAG +from neo4j_graphrag.indexes import create_vector_index +from neo4j_graphrag.llm import OpenAILLM as LLM +from neo4j_graphrag.embeddings import OpenAIEmbeddings as Embeddings +from neo4j_graphrag.retrievers import VectorRetriever +from neo4j_graphrag.experimental.pipeline.kg_builder import SimpleKGPipeline + +langtrace.init() + +neo4j_driver = GraphDatabase.driver(os.getenv("NEO4J_URI"), auth=(os.getenv("NEO4J_USERNAME"), os.getenv("NEO4J_PASSWORD"))) + +ex_llm=LLM( + model_name="gpt-4o-mini", + model_params={ + "response_format": {"type": "json_object"}, + "temperature": 0 + }) + +embedder = Embeddings() + +@with_langtrace_root_span("run_neo_rag") +async def search(): + # 1. Build KG and Store in Neo4j Database + kg_builder_pdf = SimpleKGPipeline( + llm=ex_llm, + driver=neo4j_driver, + embedder=embedder, + from_pdf=True + ) + await kg_builder_pdf.run_async(file_path='src/examples/neo4j_graphrag_example/data/abramov.pdf') + + create_vector_index(neo4j_driver, name="text_embeddings", label="Chunk", + embedding_property="embedding", dimensions=1536, similarity_fn="cosine") + + # 2. KG Retriever + vector_retriever = VectorRetriever( + neo4j_driver, + index_name="text_embeddings", + embedder=embedder + ) + + # 3. GraphRAG Class + llm = LLM(model_name="gpt-4o") + rag = GraphRAG(llm=llm, retriever=vector_retriever) + + # 4. Run + response = rag.search("What did the author do in college?") + print(response.answer) diff --git a/src/examples/neo4j_graphrag_example/data/abramov.pdf b/src/examples/neo4j_graphrag_example/data/abramov.pdf new file mode 100644 index 0000000000000000000000000000000000000000..dc7d0a61dfcfd102550f5ebdf07535916e5f5140 GIT binary patch literal 46933 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 literal 0 HcmV?d00001 diff --git a/src/langtrace_python_sdk/instrumentation/neo4j/patch.py b/src/langtrace_python_sdk/instrumentation/neo4j/patch.py index 0fcde574..e6fe004e 100644 --- a/src/langtrace_python_sdk/instrumentation/neo4j/patch.py +++ b/src/langtrace_python_sdk/instrumentation/neo4j/patch.py @@ -48,9 +48,9 @@ def traced_method(wrapped, instance, args, kwargs): "langtrace.service.type": "vectordb", "langtrace.service.version": version, "langtrace.version": v(LANGTRACE_SDK_NAME), - "neo4j.db.system": "neo4j", - "neo4j.db.operation": api["OPERATION"], - "neo4j.db.query": json.dumps(kwargs), + "db.system": "neo4j", + "db.operation": api["OPERATION"], + "db.query": json.dumps(kwargs), **(extra_attributes if extra_attributes is not None else {}), } diff --git a/src/langtrace_python_sdk/version.py b/src/langtrace_python_sdk/version.py index 01d6f588..42f8b2e3 100644 --- a/src/langtrace_python_sdk/version.py +++ b/src/langtrace_python_sdk/version.py @@ -1 +1 @@ -__version__ = "3.8.10" +__version__ = "3.8.9" diff --git a/src/run_example.py b/src/run_example.py index 32c7e0f5..cf108cb2 100644 --- a/src/run_example.py +++ b/src/run_example.py @@ -24,7 +24,9 @@ "cerebras": False, "google_genai": False, "graphlit": False, - "phidata": True, + "phidata": False, + "neo4j": True, + "neo4jgraphrag": True, } if ENABLED_EXAMPLES["anthropic"]: @@ -158,3 +160,15 @@ print(Fore.BLUE + "Running PhiData example" + Fore.RESET) PhiDataRunner().run() + +if ENABLED_EXAMPLES["neo4j"]: + from examples.neo4j_example import Neo4jRunner + + print(Fore.BLUE + "Running Neo4j example" + Fore.RESET) + Neo4jRunner().run() + +if ENABLED_EXAMPLES["neo4jgraphrag"]: + from examples.neo4j_graphrag_example import Neo4jGraphRagRunner + + print(Fore.BLUE + "Running Neo4jGraphRag example" + Fore.RESET) + Neo4jGraphRagRunner().run() From 017df27028dd2c35b9cf8fde560f7eafb2b966e2 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Thu, 27 Mar 2025 16:23:43 +0300 Subject: [PATCH 9/9] handle result transformer --- src/langtrace_python_sdk/instrumentation/neo4j/patch.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/langtrace_python_sdk/instrumentation/neo4j/patch.py b/src/langtrace_python_sdk/instrumentation/neo4j/patch.py index e6fe004e..c3633472 100644 --- a/src/langtrace_python_sdk/instrumentation/neo4j/patch.py +++ b/src/langtrace_python_sdk/instrumentation/neo4j/patch.py @@ -41,7 +41,6 @@ def traced_method(wrapped, instance, args, kwargs): api = APIS[operation_name] service_provider = SERVICE_PROVIDERS.get("NEO4J", "neo4j") extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY) - span_attributes = { "langtrace.sdk.name": "langtrace-python-sdk", "langtrace.service.name": service_provider, @@ -50,7 +49,7 @@ def traced_method(wrapped, instance, args, kwargs): "langtrace.version": v(LANGTRACE_SDK_NAME), "db.system": "neo4j", "db.operation": api["OPERATION"], - "db.query": json.dumps(kwargs), + "db.query": json.dumps(args[0]) if args and len(args) > 0 else "", **(extra_attributes if extra_attributes is not None else {}), } @@ -74,7 +73,10 @@ def traced_method(wrapped, instance, args, kwargs): if isinstance(result, tuple) and len(result) == 3: records, result_summary, keys = result _set_result_attributes(span, records, result_summary, keys) - + else: + res = json.dumps(result) + set_span_attribute(span, "neo4j.result.query_response", res) + span.set_status(StatusCode.OK) return result except Exception as err: