From 2349adc102716a77a22df6df88a4b306fabc9801 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Thu, 20 Mar 2025 13:22:55 +0300 Subject: [PATCH 01/20] 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 02/20] 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 03/20] 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 04/20] 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 05/20] 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 06/20] 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 07/20] 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 08/20] 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 zcmcHBQ*aTntxHhV+=qPzhOL|H zJALQF?6sB`poDx7D3Aq!RWq;S)$NZJns|BV93q@2I78@Js?c~v)nwH^?w6=*Q_h?T zZ_d>61q)4zl7_YF!IK9oG5MJsD56ia6m(`E7#|e5o7v%P0?A? zkI<*D#KQPgO)LZ#cc2=D4>0&GwaQT^De}II8ZqVaKVFcFVV(rV zu`S|0M$<*oOK-c=Z+%n0A=~D$h$r%@?ud~}fS7`oPoO&j>&fqTRwo^ zcW)?-pBGJpTUW_Wp{!qO+e}&(RCFr@C56OHJ1ss#dfJWIbdMG+L?Lr~@_zO2GeM*F z5plmOj4OJWK;^80N7kqXGA=bcNtYB>OYQ~YFYqZ8Sy^e&B_=E?TF7bO_F)O_hbiJ@ zBLTXpOyr#%qV!Z=j_YBO;4)y~>4x)@^G|_WH%jC+FN0ecTV_)@1E=3Q1P)HUOVyD`@1AMH1#f(U;wTV=o@J)WkSTlrIW*rzRXWCs#`-YIbKzz)r z7rTx8b%nhLhIt;^ApCWouUTTCBW_W|yPSQjKxG|ymvD<9b`L0{lyDaVxhTwZ@fxYv z-e9ROf2Z&|^RTB`?15M1jY3M{K|n(&Rkgl25Vq@KhkB2u0eD`{k3e@8AP;7Yb0X9t z+w8@fF54qie^zd=($**a$l*O9SLKB~2-xv*I>H@(3T9%%nr_9E z;K3@P^06!rZ;k?}mq8LFUU!3p0xbc;e!f}^1x4E|s%sUx>jwvh=}oF>YqFr+Zu`rx z166aw_)wN|cd$IARLTaZ`r~mRrr?h(H27K{#r>x~#{FKV9CG*igdgi3Q1!!Pq9t!*;oJm=ZsmXuhsDxVaw> z>0Cp5?;L=g>;t6_ILL|m-1_k$KY(qoq3g%8ue5+RC`Bz$V$6I~Lj2<62j$1yNj)R+ zPx<9wpL?)1;Q})*icw^s22y*#Mg@x=an#<+EJN5EjS8AJ$A+LyT0>|4O4r$FDlK|# zLjwh!Y8bltH@j-ZVssBWXmU{}t7)C&K}8Ep=(n2JFp?XFfWdp5#z`iwKj7Bc=R7bF z_JD!GSwny`+4fd`!*#P_qj(*vW8I8dC8ElCQL(MzUaPF;c`q7C4DA~oG3BH9iwsze zqQjgipW*_-CprppK+s06Bu$AgNEeJFxx*w&-@4OH=wdTn$B_XksMGYfrxlT-V6nNV zuac@?utc;8c^l!#-`2bxG$rNdLOY&!Q7I2v#33#QnZmW*P;H}jsP6S%@<=--7bgZO zl)TT3n)j-@Pw!Kje2!NQX={GXRcMlh&8gydG^Lfb(3b{1G9awi4Acm6CO3Lq+xL60wB?|(QYx}KG^P@?pXkpuFDA8!Hy_3>VXP>0z%`X|?J%@~ zCpo`c0nqbIUTIEK>U}Gf-w(8pb3C0SkOU$UM54B^8zGy6x%>|Zls7J}$_0ExkdV}- z+791Nn`M`B)nTqcc(-X*>A`;wAP@OYTec=t$3Xch>Qp7dco{o zgx_!%d=;#vg9QfdO@Z6SbMo8Hr54j< zknBjlMjcikGtn)P4{6k zpF6+`-q8TX4oSR4*hZzenI1pU>0YUtKZbfiW~ohWBsw#Ab6>E`*!%LkB~aH+^iAn% z`KD%GFw$4`dbx_lT0Nc)3>%`)45#@xw}klZtq#~5qO=BKY|I+H$r}WVDIMs<)v9eQ zBDLXo4PDBda{9a7E%cV%v+HHOa;oWvudv&7{C;EtBB$Me&7r=DY0oH1=}QcWd~)qp zd3%qxVUdD==9*-sh;SnqYdrjdRu8 zTVb0M_0Zy(mlHXuf9V5A{?#eCyES79fjKROihn73M;bWBW?CsidE-k_O~ChvKRsqT zP`1|)_yR^}2Z(l&-d7K~lKj1I_6ifn5L~TVKJcIjn9p37A{oKktXnrc44lRX+nl&` z!k`VWB3~7yV9+c(?7a(rPWb3A-0zV#jY&Qv#XS^h$SoMf2Vr^S{~*Wv$6C?dlpSfow?g*%N} z?k91Q>YrKt@kIED(&r`Ork!u_7Pyu&C}SI=f0NQ*SAYA8;h&tP>~8lLtmO4g|K;dl zY~w`0_}6Vk0y;%wM_XqHLt{q*=6@X!vbAyg>%Jqw-%k7M0U2W>bA3TuHv&!izXn)1 zSP0me8MUGQg4w^-`TMk%55a-^w{M60rWuB`@z^4*aY5Ur#{MDJwG&u>E8B|I}jtr&_Xq&5%yuZ$H}n zg+~H9VPh9_Lt{lT!T%#jRtCoZ-kAT)%Ip+ZWu-N=(3x(w?pY)v0@6n;4@iP}E^$;4 zg>VT!d7zLWG8bV`Q53{qsz%CvfMTIUh9W4?fdK|9koO4kBmD}(B1c@??(7KfduqY(gYp08k=JtlU0i=tn?FN{z#qj6WcM z9+Q7DhA=RqrS4KK4a)3=xk=AFHo?Tuqx;p`ricKNs#~X<#a~t9 zTwSRzADg}PuI=FYD1Gsad3>nckzLN@2t?(m?^s<{(RHH%Jd*+G9 zj<0o$W%T$ydB=y=im=&}79dhOeujxPav&|B#w^?kvg!vyiUW7bgJSllr3a$cgT?V< zvIE{G00{CEM*vO%rWFS==LcyJ;9UiP5@1*bbP2Su16cI;m<5pyV6_9&0{PtsYX=SE zC&mvI(l131xb1Ha5pauuS|CsnhgJliF1U<R$lxCQwDvf^3YJ$Mz{}xbeuvA~7Mqb6|0?CS) z8BH^!su!!LS(jQ5ysCf6g#$O*hq;eq=hKFv#c0D)1JVk=64mKpo;lfSZr1(7xx4_P;1;y9;~m;;77zm8lDNiKmd zDSQH-d=dp2iku{lh~Szmhh#gyJHNY7u_A1l&7xXGT1U2qT(_{dq&J;dB2(%^>cMz* zVr9H#@?C&pRQlYZ8=XD+7 z9&PUyPlM+?=ZRPuSUp&nSSnbl%p)wA88#W?8All>%%e?a8qm!k8b=x@%{uhS>DQCW zCZvs#9QoBXSEV#_yhU4;Jk^F}jcTTavW?To+6lCpYyz!XtxB$fa#D0^bh@<)H~G8@ zxz)Kvy<)wZzJK~=3D)%@1v2}~@+0zl^>g$K_Y8?E;w$GU3pEP4M%WEo>?7`Y4>ZZ@ z!m}k3B%;SRoi2oiLBB;`q``2Qv)^mOafqiiqK(pZv&&yp*+@RnxOktGS&2Sz8L}SQTIE^$ zYWtvqmx7NMiyTcVO+3Mxfz}C2t^FPUyS6pq#o)%`rj(bHH=H+`7rDo!r_!75RsRd< z%jJFRVdeJZedR+AgdL;`@)W8FL=;37oEMrA5((59WUwEwzcT=zZeEu}YY`1d@KeYo zm>ZG>MjQ3r5rdYRxr*%4y-mf<$ITUl#y~_^XsA;>c5Hr3EHWBR3e8n?Tx?imNlZq> zQzTVHDpieIz41^qN+XJ(ga-{D8`YD<#i9IVeoHF!;Bp`8;F^|6E2Z&r-J$uYd}5|1 zPPbp(o)QlmA9n}gCmea8{7AayQMcXo2xKL(GxQ_jWQ6J@`cC`R_%j|PHK-wIPB?W) zJKRvBMgmph07y=tdDpo?_ul`oJSi@z@E240T=|rFP}6JEf*`4WL>-kj6AyVe-5_%z zy->#=^QCJQJ}f3IuFj@CF5e-a{Vdm(aMi)fHCh|(vaZ_w9eYnb7qQj^*5Yfo6QUE*#@IVH z5!;Ijc?xwsdS;C`$0LI6f(m=xG}D@JR>M}PR##gljnx(#sYE(+PX-hF5&Lqan+X>? z>fXQKwkDs0uDXk#i$7d+Uvoa z#~Q^pF_txSOV(P5EtQ@Zo%>_OW*m2)d$wL>ua9o5^zK;uE&xUY4}u>H=4bEH?oOkq^`s(@$Uy0wv&xi$!6^{6%xXkvzKaY;59R#r#M{wyIGCMe%5?r-F;@)_?2={ z9n%Bqee$mTLAU+f;e8bIlZ_1=3HE+X+vEA8v&nPjz5O)@UJkFrH|fFtW%gxwc~C$O zB}bW4!B_pY^txhF-u-x)x1u-G7-kw9(k4Xrll)dIHwZdYK0_tiBzi7ZEjAPN7PaM3 z|2TX*5tX`d5ZwFgyX7T$t}@rh^`qO=V0`G-a_%CXPjb7ghvR+u+4{uj^l-t|{HCNd zV!N`3@7ez=|HJTcH2&;T?o}>1H=57Y3-a^cjm+BQc;_YP;p4wr`)|ztZQ#G$D00}j7cPO z7{ToR3G|DcmKLjoK%z%jyl3=VXImKKSNpZ#HH*uo@1+9h01)paCup^>eBX=^L}nDi zU=Uq0Dp3(5?BVhQR51xHa8+tz5(11H>ZAg`4zz(0B5e|cu>miY)(Dk;@WcJR8@20i zSJfiy@Gwvg!Od0LS8y%M0&P;Un2WS z7<~c|3RXqIN=N`=Kxku*2^@aefNUmz1`+_l0eYws#NynL1lttKDSn~WLGGSreNs47 zqwY$NXQiq2xngG9sQ}g*}9fTqDqd*7=0tVs+wYJ4&GDlHC zM<7XXtV(QBpn@j41khG^u+lNY)#2)K>D#cEqd(`1Nj?tA~Z&=hgQCnVRj< zO99D%F+s`0jWq(ic;S$L96~unLnjQV@%{;FZ?AI@o)!dJ6=+Y`B*n*CBX+%3xtzpb zK83;xoaz;Iz(CLU!9WMhi0D8`FsP%4fA1$q9#liyozvL#m$;^o)vulhbI4kpXR9K` zSA{_7NV*W`OKt%ToI+BDGAy!&+JO`bkdY2L786GP2GPv_Ya)_yk%GMHgxGWM|X@~|}T0ZAM23IQ<9JRNhQ81N>W;yoS-QAbHMo&ttVpiChORIBT z3iJ}UY}=KdN8PO=3*0C2 zc>#&vK!AeJ%|~k@pv_eIVMNFn!&}_f9|L=b1oZFg)x3aDE(0mgM7ApZy92`nZxou+#1W;$HPMeb!!id&eXA(*~;2kj}F>EIw#1v+{K}Jt@rh~1* z(Z@Q=g=nBtQs&sM>-{++wv0+O0!4}CHBAG{GAbrBhQ*xYK>95bGO9dlG)p4-& z8*H~ifr2*Z4Vu5}Wgae_MM`O8ziUEbWKZh#+cbr11GXpZkS8)La5mCy_zRNhba%p7 zL=sb=r%W96TF+w)Bc0gtwW8CnT4xNtr!n8oHcUi<0(J`MLO4zebEpM|Mj*Kit6K14 z&ZG(VN)WaT5m!)bj=>4*%a68zstkE6kWL=nBLZwziaAIh19zd%iNSCZsBt)p-gOci zx=*m)j2cjCkg<+hI$*q>@sgxPWAkz z%4`*^6*)@XweNy5GEW{cWJtB>T5aEw$& z$71o(6H4RA?8_YgwTZ)FI?pF-o3+FK-Bw{AI@L`%o&-8Ca3mJ4M&yQ9PgaMqGxV_ssF2WQ{R2u zVeHQ~eny1a1C%sAWPsp$5AInj?a!cGWv4yG8~LY zDs{HE0lP|sZsKm@Dr3d+*|UeUQ~B_NRRawJ&%v-koKS z!0|fq&_pw$kJxbTb#*^EDq=EbdF4mWU>>q9I_B*2s z`v?1$2fi|PQWR2Va*Hxq*==lh#`8%<$r3LKxCuw&*1s}-F)3mx>J*0*)ks`ruR9&kvbTojpD#J-$EopXD#C$lavxF%&wCOr<|FMVjiQ88cG3F5j(XG`cs!o60jO zGCiAg9Sq-}y9GM7Ikrq<@+%w*5kZ>m8jIVh;eC~f%Dw3NQH(A!)Y(6=H z8!0rr16Apn2sq~BeJZUJy2km^-q1o(RM2?v&>3&<8C)4i*o%c5jyEKX7V8)5&X6BJ zoU|sZr_lVm(CJgJGbC)Wn;vKpS&pPcd!mNZ?J^BNPsk$WOKzN4N&0zHKR2yLH9?J} z!t=)ae(RAmDXzL-R_ra_=q2bu@U=Hz8#2yJ>8?dm{!*K&sVVnq&FcLerQ7P+d_4+9 zO{b!*e$;BWMzu~hTAEt&uY9SR(^2X;o?9=s%v`QtR;m7Q{JMNXw;)@x+R5t>=*hP< zd>lEJVa-&v_GndaYHxIryz-|h(M+*+Z&s;$T5rj-j^3DR`EBd_k^EY@!8PE@ytKBo zbXjyoz5aGllcas6>*>2CZ>y-Q{j*{-eqF-@={e2n#=?BbveoH;Z=CC;yYr3tQR{_# zFXLD!br1s#23`SQ5O^Ob*E99{e4Jk)J47)FSo*U)NpmNq9IMo?J=}E+--9oG;t?U18(0#nZGTyWO7k z`?AiqVU{XTJ2PK?%F$D-rXx&19- zCV9Csu{Ng-&F$*(Dei)Ab9cLK6?4J;$>e@&qt#^dyo07~%aic_Xh6l`jzAE#549aHs@RMcA_&ULYKv-+TH4|Eq|r^MNL<2 zCz*U9_mMl97x%pOQ|6uUQo7JcLB>E-uxwYIDAAZVH%(wNf_RhDtC)UIFM`$|~{s#;a{?*{WF5v#&g80u1 zxW89y|FnQ(_~#9k|4VR%<39vfnl+?sH${Yl=boa!3vWcZOo*gdw`yus zE3IF$MqaT_jG4b@b9(#~%Kdst`h%bQKvz`RyeUjm-FP%HnkmZLqgl&(W)SLIu$RgA zqo(w>!^_;+FN8b*%%{+>YU?^-l)5V@c%#GTgD15;4-dYXK_Vd;nl?ye}t1-H2N5a83t=IV6*sBUxC z{LoU^$`%^?*c#Y1Zz=!d7`4V3cBiFTu~5OAG?IHPb8<6%lwx}Ji6Ba><=*{FGsHm=0uQ+BU` zuAG^o{Cfz_E*lJx(sy7%dYmOQXKuFHKFmsh?~J3iS`?>9(>Oyj$ZZ+7)vwV|<>3Yc zooi@{62gAgXSblhK<7hKB2u|UxH7w$*S+F$GZp{J{w}4b=D_9~3$@QH42q9}jldE> zh)cm7XMZrU9D~Do{>#T|&c;aUPXe?0?3b_(%O9EfqIiLNxqv9|)N9!r@@gEOuJk4` zVGB1fzAF+}+=S%ogwR%K&czYr?`I8Xeo)|$JzTaP?FN{As>ebbt5}qrYRd>s&Hygu zVrkdN6JorQ6wKB~aG}6JhfViS;9a4odP8Y8fvCV{ej3chdU}i5DdB2w;W25qGAhcQ zbu@Jqo{s#ZU5wh5?Xau?EBv6*wOtdX!z%0YrkEb76-eIJ7HkW+Y{%kUhd@>B*&CBG z-A)zoBY7p#a%ThFxbHL<<{4(_)EPr(f4q&hdtuLGyqDovaI(!v`&vPXW9H+EFJ` z{?Js~aqxu}7&7D%cD2?!!lQSzD6u*+WrPl6SEh0OTo%-02&-l&J$SI}!Sx1&qZahH zz+R) zdECzlsK;%~D1k0CdX*Y8cO22TT(bAVFO+;5o(9XyMqCK`AJ?gEDpe^C4%WZ8ZhRbHu@3@JS+_C zv%p{;3PL6q_P9L@0$ZLlK7 zdcXAL%fM3jS!62>3N-q|>c1kaKA$_Ea-;D1BB_?S9?XQ%W*@|4a=dvKpMwM-Dpbv# zK|b)vQJR$v1NURn(y2?C9TJ}NE$p5s+Z<=N2C+q!AV$Lx&nigTX)uudteNToKi$!w zdW>_PB*$boF&0Q*orSn_z|GCLqg?J`UephR8c@39t1aWJ;0X>QEsJ#;iiSCfIT zG10tmuI7cT_z4A=43BKm8ohGGKx}}oiiegOn6$CAH{z`p>NbNDA3iN8?1IXgZIeNE#gz}}Wi{BbGvbD|9_3RrlFc(8zI|Ls+j5pr(CMh)`O?&% z;aLwh<`wj(`w0AC_{V^GzcvI#WLHv?cntZ?8eJRp+_<~^_yv#k(gW9t6Iq45NuO6q z)ioo#p}(d#;&An;h~L)&4p`p#`6}amw1#+7xyUry>9R^4l8*^+Ep#rlhqOn*Z93uJ zcR7P{pF7_+;z^SHhL}_!09eK^`EuV-Noz1Nz%1MQ3YKQAZ)}4M1y+aTtq|4vS-Nt& z!h@xdTdY&buAr65;h3{hsA;7y9EEd0hl6-G-^FF1)MPzpo`Sr(oEym1&MGda4;jA> z5~I{vE|BX#1HgusyF_@&kFvu%c36jdApdM8=x`z!pt3T8>7J=f&49FCY94LYhf#SJ*5>)Fl&Ov??u_fMxh4s9g*cBh1U zdSjX}27ebDH){&`ks{7L4J(UU!9zK}xnvE8D$`s#+eYX;fx9~@nT|SEi&X=6g=kL3 zs^Hyza^MXrH)`HA-CVRoNho^QX8Z*6BKWmuyi~~8Z^unt{MjNYoLV1_ML&{RiqwX2 z&Maj@N4hxmz?)f1H){-sNeK4ZHzr8~QzOj#Zl^hsVa(XeZ5`N`rFg&8Y~GlX_}w|b zfj_*i7JllnxQCQDm~j}?Y;NkT7o4()1nXH1&^TNh-3NPM2yuq8XAGj{gU{+EW2+1{ zg4J%g0O*gdr)ajacXr2Y_IO>xS7I}V_Bd3_4;PHt$#R!=kHR)W{ru~0A6bUwek~xQ zN7FcOYZ!ozlYUk^YWVc1lH!}VuHc)m4oxUrv7PeFc@O)p-Q^fKq!0D|Irko$e$Ani z21E|Bd|2K+KyM*Gu|q$;Ex9!b@?<_k<^j~x?1Wg5Ko;?gIqhwI4?0*AG?$S>U58i> zQH6+$Sf;sMrJq$Ty`R|b)FGNZ>i>0sh2fvmGXFCo|4Q2Yb4;aLeIsUr9nI^emi-|G05{qzxtlIh z!ldqOc)Gb88zv52vVf-Lmm*=g3%74uEz{VK749EdImfzH=rDlnmdOyw6GfGiQ{XRg z{IZM%6NSkW6_q&$mcl0!ZRI5|S=o6m#>XRUzQU0O>hA~GpMI|vEtJZm1P_fV^jyB} z)7QJDh6+`YcA_a$-n%kEi?+&d8qpMeE!4O7^`Ou(OoD!lwFpHX|NgT7d*+JxdaDh(^ z#d_v;^HPDxrZ|lG#GR$7Y@OimCg}L5kn=yWb~>aGa?7}H$AGU`)pkDtJ)i);fD5BA zGG$u|!N$6sn^3(4eA!-EqC`Q;XIj@3XeU69aM#4nx>AG)^fekpbI*)o6Gr?I4%cQ6 zAaA9prjdR0hXgp00+*iid;niJ^R;M@;&8HcwnhEMXiFO!yNfh}32*@>deEjJW4+Mn z&U~dVr+g{8pE7b%@u1+ko%iua3Z}r$GUhyuqff%@w@WKS{AeA3c4h1xCh*uT|%mJ z+ehMLHS;+CQ6f*GI(@>J9Y)qU2hD?Y3R~K2{aTairn~h%ZqQ7w1Sy!^cDQH93$znp zDMZtPzT^ zRfWF2W&+DGYeAcePs;bqpH1cy=kBc)bKSL6vL{p_OUT3j(COGXdpY3n3`!{%&3led zvJ}CW-we=vxR|rQuM+ra2(nXg=Q{tb1GVDxdX2WUw5C5cV_m62iR>9Vj}G#^&xka_ zXuQbVM#U$ zjVsX*6uTGsENnM_$Q-_xewEz*WNdP$3R}4#{S;uTMII7){(y+$&Np-YZm5FDlW(2# z^T=w+J3#yn*QqylxbN(MuqF`&JBuCjHv5v4z>V%A5)2XKDqPADI;&{Qjjm|+1Wto< z`k$a%F=2_T_+I3SUefBkcfS|C|E{;qTsMO7R;T!l_uq8dXz%YjHS&4TzGh6M0?_dOyl7fwsg*EVTeO3YjfI~ zhasjnFph>Sy5v1JqrB`cWoBxlL^ROshAVV2Xi> zxIn7jeH)u3R4!KzVcx7p-QvJT>R7Hh27HhEj5F1{ogzo}1j3Dp=?|bkG9{rOX1Wq9 zsFWjpM!kAsx(6V;&56n4PXNFEU|a$?Y=hG_1#IslECzmHFq<|wj<}>G@qUt&{5my_ zIGRy_<#mVItRp0V0E02(c1C@Kbpn?BbF^ggl99=%0*>dtdu;3hm`5b8M(>S^xgSsl3`v}06?wJ|WKLdS zatPKgzLY@Xzt{;_yF_vSIKWjD+tqGN0E+!}Y5aIQ*cZ2?b!bi~o(~>a$h5yYSfiXO72A+C zr;2c4OmmQVE_Yx5I@+#$9E4%6_h=&xn}p`(us#AnUmJru_^6b{b9$Gbo=?Bo7EZo1 zlIr2?9)T9}!kw~d5&|clhry3Q1FqW4DYV3Yb|1+p>3Rj#8r;1&P;i?XFtWV#v z0^Iuk@C;_AI=~@IpJsXU@G(#_y5&S_lJ?rSYc=JBJ*14N%W7mi_Nx;5}=;B60J2=o#UN;$0te zQaA45Lx#mTBVoWAkw`KUE`0av%`-8AWkT4W3_5Xp1xGkH4r4~Dq?h(qus-`f&UXxE zWYPne>sFd_b-iUP6OmJZqqLuZ=^j_rd5kYceE~zGFZTZ%eExHb+W!il{|GbxBRl;s z%==&A^B>8Pzhlw=&+++pqWT}w{U7nk$WH&C@TqOPF^1~>SC<&P_bE_#N&zp$OYs;O z;CX>SM|6ZYUi|OFAt5*>Aq|C}BhIm(vv#3BDdZy9D8PKH%)d@=eX<;{vu7Tuyi15m zw=CjML=N!$wwctogW`GdyD257(ZVXdE;(W*9@Ae%Zg=sJUY4<_U@lJ zH?Gj!>xqfsb#+;tKIiDBY;TJ(_n`TdTH0uClRII4#!jCej#yF}iglmem?s)YucS0+ zOH1!Oh29+6yVqJDAAIQO8u;e%<(9WDYDx;7ot!3%cz#t>SDzM5Gpn6vQ-W*n%ZjnM zUM8u}ZndUxa?Q%IeaJo9;Om@V&i=&G^+w;W9mHn?C@%&2nPFhQWnYUo8*u>#Sw(Hy z&K!bYOh@LLg*YXW8C_&OUrniuU7ylhFcC;6-%s^ZhN^*Tk(bQJloS9mbdqy%Hk8J| zP_00U`nx33Yh=ox(5SFx;I|#w+(LW!lD$EvKN)H_;$w_qu@HE4Loo&J=U~fpy;Fv?+2VVL{u@tty&?noiU>d$vh@XT#vh6DqW8WG@ zpF=`f`hmw1=$S6K)&tXA9^6;BR#V8a=Njs(8Fs)-q# zF-m^_15v^AsW->k)=l81Ym`_6)6oaiX;bCZ%pl5LRTyJ5M*}r&xR9V9bIbvn+ic|l zbqpE53r`+qfN$m|5lbgW5&G^N&yFe?!MvAbhzPsA4b@ON;3XH6CACE8CB@t)uvZRQDGsVOyZ0xsmm8vM3KkoZNGrxVB=a z8o|9)Iv?aAd=Ig9@4=L74pHxx$jz%LGwIA}6dn;YhZo|(Z^a4(dkIl_yN&$vsDL)9 zBPW64gq16cH{9{Tw>T{M+Ck;I$JjZ~YR>x>5N$y&s%jwX@@p78k>VWq79{Nqd>QSw zO74>PIVn??sx1+xBKsN=@dApzvR2WL5JENESqpzy3_OWz@|ubzO0ti67oFeY7QuGv ztAlJM%{~OIyU&X}&Fe-A-To2?_}>FZ?5E&i;XH7gghc&!(lFa$E`gM~FOs@MoO3*# zkBI9d$~8Di80_P%2nK1Y^|hnwc7Fk{ttA3$>E%QsxdbIerc?^W@Eiw?AvzV-kB|r= zcNHT%l_2dBOL??DiA zx-qRI_lh~n+-_1FgBfHXR2*=0<0~{0yov$)7uF&V8(M50t9Ch0DcrmVCNg*1S?#%i zH+A+kxy`~>VDKMRIbPI0N#deNpe#T5lN^=7bH-5;*(1)_P`^f81$D+>?yOPc2m5LW zQit=ESXV&lmx1j(>P@sed|cLPVTaF_-7%mB%fULn`a<;JVt^5^lEC;`JnoOxc{o+w zrAWB6$-8ZAW~%}yJkA(w^_*^77QoRo#@B(p@gvn3G0jv*_kDF}>twDh7uI(?z(Nzl z;}f$Z*8+tcA|CX_mbHC@HUhTBa5*7ymM6s2iJXd^t2YnmBnTxKWWf6zD&63<1(V+f za*X*PngO^2)bJ;^1`50LBynSw7D27`f{XIB`p9))i(2>!YO%y7ETQeLNROuQw=8Dk z=@#}e3#NU{waA8>M)jgyhm5mg!#XSd8dQvETwt%>7+U11?Vb+9YU#tVUPQuD7g8S4s&dR;k|PE30Q}ev7(*k?7h+e`i%tMv+IE) zIGnve;|{XYjL8HiJ^>KU`UcSkPaZ^5IrjPUkrUXs}#8?ShcX>_e zpF$RKnQDTwjkqj*yCQl!pfQ9s^`L+vk-XA|6eUj#F!@)QijG|tFNcvCGKf^-aqQ~2 zxGWlKT2jNs;_HiS`USC6^h2*C>lFCXxFg!sgrY;1Rj^=Lop9XMl!A*(A9C;5x(EoMaHZUiIs;2ZdWTbQgun)uZ(7m z$z#`%av|LS$zS;{rQ=lzp(?VjJlmSbB&);1d8S5oK0B>Idt-=YkKfTN{%Uepvob)1 zc&;=ad0Raa7Yg%nNf!e*5&8M zajB0nL(|QIL6c^}kLZ&uM+O2lmUZ^~5wvB(;MkXwFda?nmOV{;*Osgz6cC;zWDcqc zuq7d|RB3gK|5r`%$BHlTK*K2WelLRMhaM;kcE~u`Fpt0lDpH59#{j(*=fDG8fIT?W5 z25StQ7qmrE#8Wu0db5ImOs9=iq!_!$r1{nA7U>qxCOl^jmU+G;Uz#Y*w5KBsnlzaz%H^*6y7= z$2z>JhkNsvIG@!Zx<|m{3}T9V&@F{0pN{$&kF$d)pM>91jsT{^s3V;z-Pw&U74FQx)kJTg#@`lNa&7ELonK(z3k@8R$hvCzT zYFda_htlNm$HLiVq}?nv13SqK*6jL*FM^eFucAEX6-CfnX&ng{j03CKp;8VUSpN5z zuuh(L66R-a;ub0weuAHfV6a@;MFGL3^0GS3CpnU;==Y`fY8 zWEh&bYp3!slhCgadle)n&8>@11t<2CxANApyfZNl$u@)udaZ=cHTM)&VlEZ|lC%5l`^DPQdrzJ|u=k~X?&zj^iEM!17tQbJi4LvZ8D!3Va|5INTG*iTP z<4#Q)-Pdkl!nHTr-meT!^Hx7h!E-Xm^N!fZqoT^3W4`4KhA*o(FHH!v49k?Jj@yOq6fl9FKmkw6GU(Npj7y%pW$rRVNB|IV7}5%4{kxfH zC6+j5kko^Dm7qDc*5-STdWphbHF%B_nn)&SiIj(rp)z$IFm+9v^m44Bvemb6pQZ=S zg%w=cIv~Vu^(&cF9PMiQ8E^5r-0`=3%27J=mDA$M2dHdG%@>i;!5@im!_ZN@X{!-y zc#~c1Z%C)(H_te&)S6HuSt5@oY7M@We)bvt1Pfe`KEQ*KF=|F==LB zUBZBBl@0)62YYBXbwZ%crcRavp?3q@>=pTvX7q~U@#(LGa1#%uD(4I7GC3B&JA?>lb^zE zP!*^66^5e5FY4h6D*J=xjbYlHMtHIA4Zb!Z5qalVj4?VLbot&-Q+@_s+oxq8SxW=- zoVBT~{~pVx6G=p04kU3%Y7s<$?!d?M_`Ug|wTbN#LwJ zDL^=%%%E@~HgniseJ=PX!=e{Gb@5e+Q*3UK@bu+HEhRMj7~ds{`N>G;cP^q2O3MXm zFZDpd*;m1Lz_QgZNAwWJdXL4}Mhtay;qK5RrqkStRV%w?k73Na@xEX6dpYSU?8CPr zW*eyW`{I`=Q1P-VgGX$Z8})6h0k3G|e@pbs*6(n|I1OFwZ8DdRv_UBCSwk7n&1`+>f&2qPsWh3a;m=xP`_{JcC=Nd zH#~r^94s-NDnxhM2SkiLftzl*nQ>!6>95p!A^wS4Ha?>us44UsfYNsq3Jp1gxyiCA zh5^u_{{a6WMFD&tWG&>0cozY9M4Nz3L3q z`_G*NWqR5@-7y|o`{C>;pMDl^IG3Y$5hQ7Zu(_S%(rUU2W71yi8j!6|MM~Z5K*c;V z&I6Ox_v8~QUy`EVf1UKmI4A@aPE2^Ft&Yv8+@?WT;gj}b!fZvEv}$tndA8D@VY$HN z&ho2=$`5j>pbG6EKCpdUP-^6~-KGQ9H(J z5%bi*XSK$GVW$@zO|1-8^$9Gyq1FQrgzY99_H5xD=y5n~`A^W@AJ=646?Dh=TL|EX zS?~X&yI(l!uh{v1)%?5Z?SCEJ{n38;-=aGv)_-;~*Q<4XIGGWD+^fv4k@61^E!7y} z#p?jGi_@ppq1mTG(I}wKAeO{@p<0_*5qY0z|8^3U?4Gg&f(AHUtpO;dRJrD^^dj0Z z%|@AAr%97EQbS#$x?i_sg=*ORH7@Z6%k=3nbEL#BA(*dX>s_9@c22K#sHEuD?)K`k z0t!!?Zlp~$p%lgO<>GW8DUHwl;0iX_C;+%apuyKnUVLp5e{531 ztn6?oBIzqtSy>6*D>bAz;ECM?w-JILDEie zPCTovf8m-aC|fSG=ax21QI+Zj0t1oRL69i+tE2Ut54`L&(Rd%#(>bK;J(xT&0sd>w#6y=>KQ}Kfa>uWtxvZIp_`Mk{h#MB1|(L1^D^bKN=`Y4k!^N& zcyNHLNH>dF{HKdB)PJxOHN4u(Jm})~xe9dDD}MGC?Gz-aTp=ncZC(~-@M5fBMA|;? zqC2R)nQqzs2s4NLvZ#JbY(9?uQ*`l++gnEK&cCiZTA5aHplRruyg!&~1HsS`Xto&( z-3x{7+I8#X^7D`4OSGT<*=7oMbbUOTy`QhnR;k0uB{SF|mbXui5M~FknIznxU%Dcf zu=3*)WLL4%l`vQa7)Qc;ahlYOrwF9)cqYe!H~aSQxPXaQytVU-UkDjo*wRzR*O`5& z$Tz?nEX2K3#-&bC67p+?DZT)rz>6hxAMhW>%^LUL(g!@{!&XIL{76dt{%Am+?WV_# z8Nf9}M5sFHOH!CDSUYLcvDvs{l*SuEcIo4c{^EIc_p?9XOe64!Vk5aE17y-SoY+Om zmjq;!Bt{7nU${RTtn=$*(puQjWk}ulOA>p$VM9cVlxYHH4iFx~alR1hW>Jp7XreE= zS!zrDum?`=+f3>$s0e2yb@xof4gaMiRS+(Fjq;fu7BYLHXJ0v$q~@d@hP@kEOsTs( z@zr$K*CDBd1I0VNLBdhgEojtza=)=Afum6SjFub`=_6ml7BAxYDOiMR$0_d&kDsT7VDE)_7NAt}Cf z05AGMXZ*=1f27Z-pwrSu6{5~Y&%m~eUE_;;vX0Mk#p3F-30{V{R?AoiukM$N8|uJ3fkmp0aDuhEJS*9C`%&wT!_w z5%$T1XAnG+*#_nPZ&|!xvGKG&4y+fdMQs{j^}SE1pW&0HOF8AivVj(LAt7SYL9$E_ z2=RQAxuhY7KCAT;PJz$q_UCk3t)QzsJB8Qr)sH^-L)f>{5Eh6jR$&3mbl+ohEloIL z;|LuDUK>G(=-9sl+M|&i8}&Jqy;Q*KinQLioBdU$qF~b z)U46Z=sOxX6|16XG_6V&Fto*LLXEAH55La+W!6XsQBcE%jASoil@b+F;TpY*l@T0t zC%Z1;1;^b97K^@I47X*vLd&V|j1OxYh*GDmVGEg2UW449X?2kD77=-&3Q<-M(H>N2 zTnEUpNOplV3$;qvwfb-Z_QtlZyZb_`?JN3#s}jCW9G3;-!$)eItpFIrJ3k)x@mYbS zYFU?EuAw~T#R~`r2s#!ky}AB7T`UP}-fN@jE*PR@sHaZio2W*S691KcPJ|U!rt=l3 zXS(xZv%q;oAhs-7cq*M3Z*7bmpq?|4P&K+L<-}WU!~syP-shxSyWX@{nNfs15hlHAQYsSS*UbM%DAxYzFwnLhT)d7;YFz#m!rlEg<5lH zY6wsRbQ5rZ#^ag5UEpB!v3_)^bF9+um{;80;~I~apOA5m@Q_J#=r}Ist1WaudqZJJ zz00-~dYInE{7c2E_%?zwnI{J{beQ{gc|W<^jqWBb9GGe}4YF1{MW}{4CM&1V8IfCn zSEb35%|}+I(8LH@u%uK>sNNE4fs)8cu1Jox>?cWGgma$I=Yjt zB#qm0RzkZ>ArgOKjo~|U=wx)nb*?V9VW&%iGWL7wJ_`W8o^Jv)HJAW7+H^+f62JFF zeFoHO!D#K>c8N8vE#piDdIL%8aXI-S0I;c%-Ot9ym}#1FkboWJR)Vx?;`nqN@0MwFA!W=H8P(Nd+ zz=x)RZ&|2X4$bbm)ui<~V z04{j6)q-F#ChJTP#<-cE%X=AUYgGlxESC0j@v6)+%sI}(VznsfCM}RbxgLz#&zjU! zR$rg!!c%7v{~ca3{xPudFTu-yO~J+ZFFCmWOXDHqAHxd&*Wu+KjiUcOykueg7vtgY zZsdjE-N;eDxsgw$Vn1ngiY(*T|KyGy%w?aC5t@imPVjDqz4CTDiy&UE<*%gxWq|+u zt*htRcD&gc_kf5;K6b{9ckKEbxk+`#l-Z9oRFSOs#G6pgr^g@Z3Wp|p@6B=VI@N0i zGasRiG&I+bw|3uNchk8?!VisDh{o7D+ANCS^5DkETuek=Hh>LhWU8}_kD>7lZuTCp zNIu-i&KfIehD#9*Bx6RKRxcg-B3b+KmQsAyjW2}DX>T6O3Rz>QYz|WHO!DsE)YaHx zECQ#$Xw1arbc89MMR4F_7U0a`pCaEk4s)o*VjdI?-DP>LJ|(*a-M;5#&=Dnk*EjgS zoH{|=5++>)I2-YR5^XKw{B7PrXj>dA`YfK9jl8_XqL&`P&@3d}@sk4YuoZgXd=b{q zNZ@Tu(dEr4P!(Vu`Mk2DyIILLh}Y*c8l17>(z9iLE(yh^&vpr=Y5{g+QVg7-CrZP7 z%?QyhPR9M7LSz(WM4#hdR-{S|R3z~%S1e@Kv$aVaiNi$#5CA^%FRqMGc+&q!e-q|Y z)`~x8?rO9|-w};WUXqEbQ zF$AkoP$F_!DF|@gim1{CFx99-*3sMKmLd!k1wl-IA<7}O@_Y0^%B`ze&{;*zzhari z;ZGsb$rwG*S0)%ZKR-Jt4hp)|D2Xg)KM;z?-C#tG(z5Yx@9|e~-Vb?fp-P?Qx4Z)g zYPey$BtMfZb(<|=Xip|P%-3rVD(%asQ9)T+S)rsQZSDH*o||cG|1~` z+OlIxqnFr(C4YrCiNg}dQ6RT3sV@uR7Fa1eJG32Xti{4kc}WWlw1goKdr?UHL#7M? zKQ>qa2o|VYuhejCRkz?+WpVgksxz#6l__um|8-!P00pQ_;oHSu%~Fs*naB`-ds5TiSkHz zqP$7C@{JO$gB4=mNcB;sgO#0vrMx^A;BZc2uPPn18*%?I?&<>qPAxZrXA@!EVVQff^3GWQOJenhU!Zt48;v21$B`NSyAtnV3#t@H#YudXmw2wC0v~1 zBynSMPQw9og<*xXB{5wzM`*Fs?Zac5e1ve%<=;Plfid3beI#F@PaHXj@UMq%0)E&s$*BV6uH%Or> zU%76RO6>Fl4g_<+><4*+^e^d@PeWfdLp}ba7XZy=kOZ@aLdKoI1~mC~p%dc)5Pa5M z(=o>ktc1lo7E>R0w|7oUQOmp^z9a8`piN)N|{U&orNVNp=LSilo0QRqdfLpNWmC_lSi zE+?F1fQd^0E&b}DraY2dJ{V!A=pWh zN=^Qoj~YDX0!s?7p#rFCqc@-v9ixQeoYqHi*5Khwl#~qAP|QxC6S|8%l-JKeYH|0_ z7tn3=yN9r_*=%CE7^6@Scx|(}L;6pmZ!Z=Yte9=(EoT9O)$4K2qZSvh0EEoQNdAM@ z6)(so%Jj2=mc}dc*@r zfcDYsz%2JLaCaB5f_;&R)?HOSuYT@46F8EqFhs(kN}<0ddbK`?&Ru2!=K|qsap){` z&KT#UQ>pUqA&^W!*0e~hRUR)sFiBkC%In-a87OXkPCF*vDk3uz8@*q6(Yn=(+Ziog ztkP;z_jkBK9nP_4j~~qL9FELWl%SC%@Q8B6NHt6}aJEH7#VvivC$_a(P@_h>G>u#T zVtxy9fV_Xp*k+oQDd^&g$}9I}(}0>~kb54ggS=^Q@u1BIq%yKetLc#wD@ROc{Q~F! zze7S;VWWMPe7{=fLB;`{Yn$^$_x(B(!}>>v<2GI~wV~r!k!cQnhmEq=RTJb9Zfh6n zKCGlNIrA)$QIfG>+iTL-hlLSUb}jx3uwvs8z6tp!=PF9vd%ByO5{}p1V?(1&L#!SD zg%d9miOfO5Bv@9d`xG{%^6tV;v^RwS5|UCTSWg(n$ZhO-L|l+=$nbP7mNrLf%mwTk zoIZg_XB(JN&7mU4yga_K8fw=*%lX*Py5^xsLt$fuaPV%UrHR_mwjqG;>!d=4q#)#Mpz->)U%YOg9=ZmGT)rPAYb%u&WTUNBBP zzC38VN^Rf#38MUCwBTPtlz;O+|HEMUd#=_0?2cyqW31r+I->leA@;vTl+5)1YOws^ z1Ykv7y;oT|*`ibgRJZvRtaecu%&T+p5{3?uRQk!#X*RE>8iluHyQQmm`XjH2j6MF+i@pLp^n4lr~ zikE&(XRu_%q)lROMxjBCM`w-c{m7_UG40y&qaeuI%S%n+Q3bE1>G$>3@{Ap;7Rf`b zU0@JDJX$=WNURT!d8}mOhzmSrO*z*hy_wWkX_eWYliGitKZ@LucsAz=sJ^^+5Bhzi z={`n+5UbDg9~H4C*+jLJjy!k9gQ7H7iaEn+Zb`h^GJ4O_I)6Rp27Fq-q+tW(HSSeo zO@O+MMNn9EinJ)O^I;k+%vGOiyuU`fS>J4WgtHw+e6r8b+6<&R)OPFV%cTW3ULEq6h0|fq?O+;|Lsr_mBrP7r-%+3Ty#32xTfq7FRh3L;MiNaxt8$SJFDMoWnv|t&S@^+#LZA?CB)+_+*tYPKv>Eh3)q=-O?+&A61e{(qWkmRw3 z8zkp65w%>$#e7G)cV1x~kE2e2nW5^-ES)Ik*_kkvDX4;lzztOd0uc<(;BD`M4n?~Q zh+&HF4{Cx<6iUyNzqO8-*uL1@$zjmPtEb(&foYPP1$m86+`*fbfUyOP%_i1#co%bs zSSA*~k)Lx7titz@*9;P^Ij3BjZ@A@AsJ_C*#8BI)*>S=EUtwYMgET$P)06e}vxUN1 zeM(Rxi#X=mxLv)ytWW{lS7YNUoVSW_mp*6sguPhiB-|r;z(WDQ?pQBM1pn@f& z90fNt_vR_$87>Dw5FJPkYMDALZ1~8D_I2+xfx1oKC)iY7IC0aJxmlTH4P>j@U&3UP ztMvdpIoHBLl%LFw-$1!g@SRj_y0%)1TGKE)L3OD!n4jM}K%quuI(t!eFY!A9(NgjW2Frqo2Bth*K^t;OH$xb=gybV3jcWfFCins`ZAh86Ichx>-4%oAV$i^I>A!0 zdDMv>I*bun12W_YThx=07ir{YrD(6N!TafYCjeh`$Rg(;5&CE_Hv*=3(C|jYEJ~9L zmgR|>3dIWkR>9XfR z_a{>w*Nog8(O{!eD+h=Y*liI>-y-pPRpi~N!Qvqc=49f0#PN3awf zI`E0vvDcA|o?cA!J~dp1T?pCnu)cHZz9r^Uqp8}qn2a|f=bVLLNZaI&+m5`U3q4c%wagX?l0;d%>#R4N`;jd@-?MIIRJ*?vKTZ=&69YNLY^+ z<{w6+io82chLXbNxt(+vPjFo1Q;dMJpb;ph(5iYlocIz&O$tj;WuTSsSY4y5v5Ao~ zlaov3IlbhkCkoL~JyJbK5TX2x9D7FRhVKCM<*V+ZC-E`s+?~ZZ&Rr+hvwfeE)QWM> ziBCxR5Vz(dN=9AZ0Tdf_I==URgzOy+bmzvk2d;TUq7B0IL)E$8Olt%`0b<(n4=F`! z#h|~Xew(6bwdGvNb*zffd$$o*1AltV8m~vOzhc0v9yr^QAtzkeIN@zr{EW|7RaAyD zY(2sLC{DG1!&MHMuaZo%v-d;?bTRZ8^5CJ(JS4l zYrazW3E_kxLcV)2_A5zlp$@-)P8Y@lOEjgKAE zgU>=V3K#rM7m+`mNbMrz-n?3f`9 zkIWs&_{4~4Ep4w1e1pNaR+!MJnPdGb_G2&E3FUr|y)M@dpxa4$Uh-(|8%^8Db}m{W z;74dL+nHG&)9MlH7Z~asPVBTxUnpgupHjeu=S(q=B;;geJuAnJT#L@vYw^)z?0Gm1 zN*vzssp3`hP^b|L%)B#IUHmEtoJ6jK3X4!Wa_P0Lcbj$ooukYq(gZcPiRf56gvc=R zDF9p%%iQOmxb^Xiw@r5diaNjYWEH4ExKX4)3dC(cu|BO)YXp7U+-Js}wm}Rf^}x;J z6OFx{x;9FEg{fkIy7Gc^La0RK3M-XovY`)O>k-%FZ#-HuItjClzfUP{>T&RiMiu z$u_S}Di=CU=^v3*ExTFVOCAe+=30nk&EZ#8j=;*nh{An=^0?_`sxI8HK$7fRHa*)I zkS$WoJW~aIQJdRtrWgc4WXRIfRTsVAkJ)>`X@jW;DhhkUzIVn$=@?A|;N^2_pFF3= zEMU)*o1A!)HxFV6oIoQ?UgL!5>qdJX>Ef3g7`+O+9JpjQ#P;ZN{jnYY{jdo}Rl$Y5 zWe4RoqkZ578OsoJoDc?fx^t48x1ney)9XG6*sh|{xl;S}_4L?$P}MAz9EY3wGn^NS z+x4f*B5yzeCeXk?fuVm4Rs1V3lt=1hOxZ_dv4PlnBU zwGYFlDC%$PW1w~jK(Pb!i6z|_=hF?SPDm~FQCu%aLa@V-VzG^kOw?YJO=I6{_hAb- zQ*~;`^uj(h2v2QHy|VAGaN!*oKkun7pI%p5oI_)6~v;Ok9oc5jXdGZ7ZP6izAg@4GP zUR5^U&<_fN$KB(ZBk5Z~I?OK^DkJb43|+TR1HS4jEZ9$n@s5=dkrOF#CyOi)Dlb7L z2`wa0k&1&G=A8S8Ld%SYvl29?R9f=1YdY16%ei-R7x_kYc%^rprDgTv>?VBs?w?Ts z6f-;}04F0MdqPqlSJsv+$~*bvb+}1+ill7YzLi&U!)FzgC*b%xPR_RA#h9;m@-BfN$(nzXcV_g0Hl6|4 zS^cbg1F+`F$ioHzIeL(1k`Z@zHeEXw?8~L;vw-sSK;+BNB@4bi9?a}SzMorV6KsQ( zZTjFoySash*f6MaQxNc@MWE#dR|J@05~XG?5r(XKZvGM^iD|CE%$R$QfeLNr?4$x|*o zMUvs^3nAde{O767)J_m6KVclbi-^PhJb5fdUpnwBAhMI8iI=Aa4UljYVM@?yp zR>57q`AlA3S);!36i!Ykw*NyF9z-_$7OaJ(JOu&LAkh~S`^L$<0Ec@ z8xr!~W6>U+b*xs5e9yl*12qnkOE46HlGqd^QcqSXseH=WZeH5j6?Ks0zQ+wNc0#_F zt3F$E$nj&=WEPFOuGiZpCa~WXNOAR(h)a(a0A6(tJA@AJh>qE;!As$X<$cP;M-R#` z>cT2edrkI?R_^UV4(YnD(KvjU5iaBr=Vk!iF&aIW;Jv&Rkr#4oV*rYtFzhh0J^2*% zp@uk9?jZ9eVCtK`GasbY33f&I)E2^`a(8}@CqR8NlEAWP&J$EMPII6xl%bpDhKMcN ziRAWv?jXL3DLG!^Pna@G8=`t%8~Rlp&R*+TR{45s6yhWXx6Twc>`xs^s~hsBCMk>L zweo`UZ(PiN_u_W|(qa?T-T_11Gd&coX;|}f7xYzx?M%p&+w`GPHkd~Pt(XLW(owi! zSJ)lH8ZO8b&LsnaBEU6WG(z6HAfmVk!s{#5J?wBGJ3iIHn1KQdVYxmmDH$5UD27yh z8(&zM*H25|#GWgz)F#`p0`hux{rP_7L&XoB4CNT13*dvh5LTV6`bQnM{HlSziagYOpCO ziHsg^2~;C74;Nn7+WdvEQ0T5Xn}JDLR3fH%QFkAKYuz<2$i5GY8$)=&%~k~3zmL$4 zX-5JT7DNB>8vh{KKs^4^O1u;GJi*@=3FXd&+fP3cs1<0Qr>1*u(DP(=tQ`?N8`wJP z4kzrn&I%F44LPHqLSNUiiGX{IBConu$E7>n1OaE8I|0mjvdB>!#O$J!>B)V4rhJR|~?b z6+G8sly-|`-GTGN<>GD)$Wb{{|ZASR`>V!2#^m2`aIk_U9 zy#Q9li@-tn8RGm~v}lJ%Ni44&npsf$1NjracZyBMho6b>FJWF~hk@EweWEFW`Cc={ z%DC@$Kl+5%M6_R$VLY>Biv$?T88G#FdG*Gv#41O53lu@W-8aee%4Ot;7FV5B`H8x2 zlI_{mxt^Ge3aa-H4%VD6L>1dwUow*pAcBI*OO6`AH*1-#QC_d;2X-7*s-5m0ax*_U zEpqN|$ZV|2xj|X=B7(j^qxd%qVAYsf^{>XZ71r+CoZgOSgkvGy0&D1?e=#ycgI9p2I4J09QcTRRlrqqwuU<8SAWa9 zY$~{xVgb)NI`JU^jF34o(+Cu8$9kCrA6Z&ZZ6Q>uqP9>K6ax=)E}Lm9Pq|THvh7Q!0&tK5OB9#IeoBhTXyS{C6XE$>Bs+^76HQm<+xF2brrN) z2|CaI&=rcH%-2bY47s^d9foazBFSdgVbdx405j8XNph zUcREFH1^1yqEoXZ9k34nC}Jmnr%v;z%AVp=cE%aOJ!1v2GB-s@(kF;{?7Q8JnX!o* zV9!Da+2cL~Ub&fsq@f7q<2hoPefuMpS7{p0Yo`%QHi z?@6V79yQ`|%(-dj9(GC!j~Z|p+MAsen^hA5{?oU5Em>QE6hGYBJp72P55{1fWs7Do zPVLghUdCLcP3luBE1h6GYnLyb7eCCIVw%feIrKF+zx%4Mt{HTF294 z=c1hA7kfvh!eG)9!?MI$`tD#JNM&3wJ#HE(6aXq z%;Uk;we>@{0lYsJ3G*#EU_%TIVoI3-LN}msyFuWCMF9!@=4=URz*Mzosu^8H#ALqX zQrl@>G?Jg}%#1AJFMI$pRMS#9@U=x$0|apuK!O}-*UIRS>BZawbrqN|RJ}K8@}Vzp z3V9Nl<#_|Pwcz-eVv$QKQD4-KuobY}TbTR>6}-5Y~mstKKCI2sebpmipv+P zNHOyAj;1au1(*$9JCP|hpWv`VtJ)L3*v z#+rks%8`mG;6X5%<*Yvy9u-!_`4% z+-y?{BkJu_xl>1L&>hwdI&q7%?#d_$hxpMbW|U<=`RE^uKpPK0aE*QVyt3h;h;cty zTSl0hd4$76as-=``5mqu+NmKFh7RI(M<~lbfTkFxY`R$a^wEO*9=~8O!2TiH^rk4e zJeP4LSL zq3?4Y*JMK$k`u2(4F!)54J~^cL@bCA_B4j`tXU|+FHGS8EZ;X8<_gK&Er`GXVd+fz zy$$APTh3L(-wykf1m2gheUYMuclkYO8bZ&n3!-l5SD%7Hq|k;4k%OfDrKdEC<{OIo zql}>@7yTZ6-}299W!BQiHD_9}0#bL8Ur6iEs68Ydo)f5rbahnJj@`Fqk=!(H>7^Qk zRxZAxsYl9%02Zv@&Z{{eZOF1$>Jn(e8wbMoiT!5+lXt)-YLCUPK6(Fj7WLUm~qaa>x2 zY)T70(LBIJg_i{?SYb?SYXG4C8n)F)dUC`!kzTr6`LN=N(KaAfZWridsso}L5sMzn zs+8W5^#e_4`UYM?5nOerjf9|x%dwJiQE`x#Z1AMHyvc%_BWtt!e#+fuR(qC)djukF z*@Vw)eh>a56JGu;A9Z0cgl83w9F7NmSp-mle}ALR47#jMW*(J#kvdy0Uz+GNDiF}Ldmkq+{uS^J=8i$#zb_%ddW;-j~Y!l8RexO2H6I1vYSliiOBQhxr53#SyuFPAdJ8AGzb z&;-mfCvZz0r1f1U)KYTCg<43EZ(?pd=Ns5yhYJz%b@R5FRcS6K0(W>D31xg_%ql`b z;>b@*#(v@*$2GD@RX=7)c0k5Kxt8JX&YcU#wYSU3kZR?jC^$5)2E$rR4cBSrl?&!O z*jRW7BlMmWj%|3Zet@Fs#?DR_E3{WaFL_@w9g83aoII~3nRujFcPwvNZWPe>RshK4 zx=L=gbb(BK2iKDz8Q%%9AteSp_ve2eD_CC;?oSCi(FV*K zcyu=PO_EU_YlEbSE{i^P#7s)nPW;KOlC0iiki6_UqSXv2a_aIU`h9NEfSS(&A9L)1 znw;7Wl5Hkt9Pq|Or~&TumLJzm-6tETLMd#ZxIe@5VF6bsv^uoEtA zn`cNy%h-$WkBR)FS9W$^6(?1U+N6g}-=@p5hHj{k1tf*d<@}@Vq+26|Cx<}JJ5*4Vrz$Wv}Qb2PKFN)~TY6vcgh?B_B10h|Mt96Zqu~p_BKixY8IwMczLXVIS>PU;Q2Gv zTD4!K#5~J!$>g+ukutc8taI&ZeJhsCw|Vb%VH)*Z@!TDHY4kOZ>orXKG6MEb>4^RI z5llf5abInQby-jk7~Jfj1A)1?Rj~KSO7Q*c9R^(>ZO!8E7PDr5N*$uId?)YELl=Qp z_m3+X{G2v(AzNfLZD2tH!C>W)_ z^KcSEDYMMG*A6|3M>%&I@0Um|Z?}XN`2telLBp$PouwVflRnr@vszQB(T!Ozba+f> z^>_G%_rBzvAiDTNrLRz{sH6s{(4j@onP;APt0MT_{Z$!brD1w>)|8h6S4_aP!d^#yp1N#qd-`@}aO3|u_&+vPU zGQ+PI2a@4;!~U<(XGMI*-}7QI{+<*;^S8;%-;%EWx@UhM=Ran1|5e)`Ouv6?``0v3 z5ufRI$xQz)`B$g^RPx{7>3?Q2{%tkiZ^Qe@7yI`%exFJH?Xmu`*UbNZ$AokpbSrfBa1RKF(hp$8?_^^&EbYGJmmiH2nR029^T77guMps|a+EUl*A4fz@?d%ir*E{iydpmi6DX^{2A_-roOc3;92~`fvB~PwV`*l71V@ zA8Y;V*nWT3|D@-CbcFpwYyY8qzbm7xuH^^s_J5(Q7P`hC9O55(_M1uzeh`R5YS6H- zu;bG(v9aK@F|gw^&@(f9P=iZ+yg#P;0#?Qrh9AA86R_9+U?0cl`1odgFna%9pke&@ zw@fFbYa?!GYHZ?w&&J3BN%z^o&{7$ng9D#V^>2q+nAz}u9TvxD|7cgh$@rIw|I*x# zcf;imrQiDW5ocz|h|m1XNc2ZK^FJ=7kT$e3b}(VUXJBDt{^-!}#g9(~J=;f9e-^0r z$BfOZ?yeqRM1ES$mDJwGt<^Z7PA?tsgA-r@0s*ECqOvXp1jq#V2rPZ&_Z9GyUPKIm zj)6Bo#t?}dNfzh@kmUy-{stsyAiTnB3rNHX8xFkM=XvDdx4~xbeLvxD?fm^XW2Wf1 z?Y3g#S$TIVcwdG{Sy2-GQYO=J{i|M^1tY;t^yhs(Og!!dJTl?oG~W9+wJ!VXQugRp z?>)c6E_*R|K~7$E#?$t#4UWsab45R>F8iMtp38A+(R$dIHSN)ttz#KX^}FuRl3zWH z<~^euxwA8RTwhL}jbxCVk*4i|cz;f0UJvH=Qw%NpdA+X$7uB!GK|xGy`K_nr(C!aVV? z!+24&cb?a&aVNn)?nElF2_=Hs?nW(CxqT}-OOiBwh=M^%z=&B;_D-j8xbwH0BHsyh0TvAS{MhWfmqb$6`ibZQo87g=lmyj!Xg_OT-Q#H_?) z*RIB{AVl+WC^>$K&s##}%{B8iZv3rlX(Hn_p^rAO8n0B9N!((utodzEVqNAj81Q5( zP;XZcrB!~kegCA@T0h~O{h2k$vuEiMY-u6m^9 zzLqU)DN6571*_D)`hDxf@eHbJf!#f^R^@A1Qf;z!4F#QXb+W}t->K|w-z_o&+P2~i4-@TJ-b_5|a+MJCugW2WWqr5TsOnG8!d)_*i2(dHE&@? z^(~2yi&7i)aI|toz-(KP%X|*9V>O_|TFHW;4EX};Xb9rmoZu(UTBV)O7*h+#jwL_( zv|aYDHBT3J+ZWibxup3hyGx7KTB>#RC~fD3*BWZrG}R+bEt3Zd35NN@#v<3 zk{$VYWJ)parXeF~O?9j(=h?2;jAqOuAUJVM9bJois^&ay*R`>tH&4T)e#n~QxLYQr zo-!R+o6%KfFIqS4N@fX};Y&ptI9O4RT3qT(_G0_)ps;3*)sAkG|#T`ubYpal}(6 zlGxgS2WApohHyE3TmMurxjP%w5jCuf1{h<87)fd5@IC6VTJiA}^sd!xk$@r!ghy)b z(cFNdYI-oJKF?g;(ZKaqc6+_OagV)DBy8C|@T+mFT1qFbS}<2*C z6g6;{#?K9DDM|;*Dofp|7)%WIMqxTF{-zlv=<2)D5c<&Z`U|N6P>Rl?0su1ly#C|ke=mVIfW zVhwlD%#6;bD%j0Bt)X^5eJv-}h|pH4IUnS{W91NuijlLu!5K4rpy3fgJV0|9|E@6+ zqY69@yJe4Ht`o={HBLA&v%s)Ls3^A|LxSjLN-X-u&l_5b7lYzbNj4GB!H&wL3i`xH z#3jMeEX^u`Hpq^BP7urwC-%UyCzw8{6!C@XHlc94T?pNhX-PUcmh>t_8h)7LQPMO{ zk>N;o)F+0*AbdLC0`Y}^J^v-b8a!0#c#fV1q87ewS$4fJb(-D|vO`-~v=-uo;EsYO zK7MCl5k!#!fi;zzKZ;7m483GFPP~9KlQxD7>s$ogGWI5gJ9Rz|3f&&^8D$?L$0D3T zU(iXCC{cHqe$n zwh$_Xxu3OSxN}-fkdkj6D4R*cY`jHm;f$TUJIYE%&r@85i)fi$Xu?!xPP$3>5RoJj z0s9I6phnvFL3-H` zLyni9rM6VKl*t^mPgCZ3U>+*Bbv&TyUZ@Jp!;cQfYTTyToY>@vTb&WxSsf#y&)M>< zaWW&2?8Uww#P|?N4LrQz*S-&S=YM;cyDWtYQyI9ArfaT5cfjIlPibf&$6-QOVh7I7 zgF-s;5MgM9EkZ#FEz;;?e}J&k)(k+vHNgPGNz&GX`TSdaMFT4~(2Ut7tbwHLtKjCwS4o3WV5MCkbWK2rcE0n&WEsRVh<b4hBVHR<9Ok;CI(4`_gv@lc5%?MJgm^Wd2Oe}%ne zR2*&4CY<0Jg1b8eoxv@*OK=J9Zi7PzZovue!QI{69fG?{un8X6$#eGXd6RtKZvX1O zPTf^CRWo<@nXaqKDasnm9i>=bw^gOF9M`QGCUdaEpLyDYjVPH%?bV|s#x|jrD;y!; zC--iMNOW`Wbf_#{%0wh6r=hK9TJkMd7}W==)V>%_szi4wA_)buFsiURaqT!1`{~TQ zt_BrpNysbJhn>U@OlirR#IQixG!P9>9`chYSDb81JPB1an5tdMew)%qO0YQsO#MsB zmwnTX@@d0`7yQNvE)I%48Kt{kc;KhWg)z!~GR@3Voxfdx?~CR`K%Fksm8s66cys4d z(Npt`d`d;S+V#Awwz+M?%@%?YphwfaN5oRijI#Cp(sFmp zsHcF5os>`L#=`fTgfkzn;PyT}>z9QOr+4vy8J<^(zmx1xKi24Use!WEYp<6n&&)4hv|oZQtZWAB=hc&jb=K% zV%1Y*<^)k7#6ohW|2uET#8BRnLZV3y;ufOvcJ;+UkAUjVi*hOy+9jHWYj$SDCBEfm z;jUvS83P!Qf%WM0^TswT5rf{VcZT?f(lL{LD`$mIK5ZF4h^f4e+MTFoS^d1|Z3F>k zNnLcKVH`6$nw$A`s+_GAvq&&|LWVww#hGc$Vza19x&~0|-^+mw6u1oP}6YxK2Q98J|pK zE~|?}qBEB@yNWTTioqmh4cW{7}PelkTdW$L@0#v2vAKO!qNp#|E zZKs!N3Hr>WB@fUx<_RAPy{TUR^t{opjK{-lp?N_S41`YSZn+ z!P5;PKdoQl^sqPMT9-M&&c;|$YmYi!7+8y{M!A55!Xw#6lK|z1lQG>qQ+?gAkO{ z7(Zh6Dp5Gs{~&T`K9fH{12ibVFxK8r@_65UCZ>3}Koh7L&Y}ma|9_`$r4!9L&USC> z;Ca!BaG%5zpnQW$5de2`wI3Bi5)$Zmh%1zc-0UPNMI3SV3x<`46*VfJz_WX!Lgh?o zJGSr|W%OwIq<&o9^b=**`Kxvw zXhqQJs@P-WKcB3`pm$)+43rkhn5Y7s2i?`whTXXJQHtM`#z}KnC7Ok$3j7d*wQCao%IlAk0KKDKEP%O?bYSK}$wLK$WF8SQrp3B}OFkE~n2Z(O;ZQ1Dw$?wulT-9-#CR9@)&8Gt_3Mw~xe4CtJ25%L8 z-9~PR`8aLYKX#a|UF8PH!TK_C%YIdRSFz?rDD*_$d={HNp=O;~w$$OUKpC;zxPeLf za+DS!whdz~_KAMd4^{di^uB|ephpD3nEXCeM7v&ONd~h%HB2%|t+l>%VU~bi1~jgsv?rOQC4sX~aU4TcFZ+-+9%v0JAywhWl?P>c zUQ34dnRKbEnupbLdpf$M?MgT1`hqc+M~^W^%3$W(q}SCPgie^N7HV$YX&*24IdR-k zxnnZ0$xo~M;g+Gu0voOndJ<&R%QiF{oPJ4s0v)yJE2KY}uFaH~^9cE7Zg{&|9Qg<3 z%i=K6eSM==si&)=TWvLa7?>gJn$F*BJUkH?*gHug@bWXZT=FnsFw1@W5+WV&&N|z)(FvSi(+_WO1b#|1B_il1p?(n2nIvy+5YL zu!szu!ZdiR<+B@^Tj0y7ySrgrO!B)(y_Qb`ea+j96N7-W}va(W6tO@ON9n%r!$v_H1b&`RbDOqC+|w z*|_bbmNg8sY!WVzf`R7o6ZmAk_4rQ*FOIA;cHxZJ_vr7#!BN0ngl&Z zyz0SldPmV|bteE=+C33%OX!lLs-&>CDAD*}5}%af0zBpb04{SK`o=CIv^mNHbX2?W z=IL6J6F{~7jul#WSKzkm=Y93Uq8BwSGvV|t>$t8w4y?t28epW&7-)yf?A7f!Yx zNelfPes_O9Feq*?oPGPiWlXDU;0%SCXX#fM9cFm20C$>=cB>e&8Z2s8O*NVH7F(V| z6XcS{XL1l0cBLtuH1&=HehP|wWW$}&^?J4&>&BN-++10~pWnN$`u>UEK2MN&xh z)#ibaI$x!-$Ph*E6!&&(e0tPkIV{_aE2HUa0Bo%S;I2>G>UGZNr3o9q?sVzWC~5k^ zDAe=d9Ryw8t zql0$eD=Ic#5ekDvEo>ydhqA=OrMGZ4=a^rIq7z0}chrBv zeYwe{i>SZ22xSYg()a(taReD(!J-VS8;eKDH(nAo(}rM~JC z;zLO)8||S+n@&LJFQ;W3_QWmj1VsbYS%BEG$X{U}DB|Uz0%zW;Qp>N-DI66QSPgr+ zsdvE@LDyZ{lqnbCSQ-C>UlO@#+7HGId1ujpm@NY^xm=9)YoAFT<+rMRD-~8pfAPgP za_-6d)%Jk*#Cw|Dno6?5dZbvgV$+noTBMmRgPF;1fhN03=26nLzxrinf70|uEpNL) zzr8Hqea>b^6sSS;X^~H|2_`{I3U4O~4pGbb00l@6n5RnS_>`Nil`W4^MAjrfm#iH< z#w6L(@%kpmC^-dC%#>RL<6yZlTDfq+=#_x^x)$`)GdOPdC55NL*KCXl3mEP_EhYhL z2_!`8nYI5mZ8W8+n1?e`x1mDw)cuktHx$uX(7>>IeyBnae;5lzo0N5}BglvA7}zILJhMbtucfT7SRv+l~MKdZ59 zOZ0H!M0K0X{v&@$9(`>iy*x**(plYoHWgw5h zgjC490)?)@dy`TC7C8(y|LgCBksleq2e}2uA=F~^!#F?(!H$=&O<)!)M=Vt8W439k zeQH{s9?8LjOi@ZUVD)3!H1`oomeR-|V_3E9Odb^xixJIH@ z2SdCAO@I|uC?iwN_7~RTv9a{L$jFp$H74d}$xFuL@7Z3pS!0d$qV$D)HuVoPLMmUB z!)q^xwaR_6P^E@d%z*Qn*Evo6M9k^6TdnAdq6_?)VGKG^D-p!yzFcalpI4MV4;){b zkUjK2pn+4Tm(TL8Gptm_Y69)k-`Vyjt}-XLQcnO|oxm-mWZ!od9&yPNz`$1FK9EqR(`R=9kV|8xrBm}!Zc z3~nE1Z0(E3YU;!rT@1UiGW19*j9Ds%m^{DtVon&h_Q%LabQilIyNJ~Y-OMU4<-Hp% zLee`6p_h4~9dJOjx0137pZ(exru}RT%K(sWMes0I<7R3uE^lZodfn!Mo#{+Yctb5> zekS?OaSai;sLMOI*mrqgAiuZYCMLm-!C z%tYfC^+wxltK{tu0ncttSf=LWNt&tLIPwlG1}Cd+wOG__Kn)zF&JZoq+HefIQm)jI zOz==O>b32J6}k%RM5rdFTzxy#ig9&g;JQ~FT1O9jR+*Y|WdQ1SiaUOT0k!`914Ti~ z7|oK6kr{G{UR*y^>r_lKdHr`6`78EenX)~5PUxS(p0V$rV8Qq62| zy$4%xpGZYl2;d727pnG-1r^oY%$0r(=~#3-rL$__dZ-b5;c4FTWbhheIUPan2yK(6 z74J}xuoS&gMqElY@K*q_`W@b5PzZ?tRZL3Ic@!^W_{U8SNyem}5VI~wVKKuUGMK#m zZc#F%NuL5~LzM(%waxDL|_MMF!Ygk9t^fo&Rsj~zXc%lHuzo)sN;%^nF*G7q20RNfF?S#=hgIp9>0|Hq=;e>Ry?)lm3@t`ahuALbP{?=@GRyxx~}s7iK)L*dpkdz z_OM$}Fk=TqIkzL`?U*fmOrnkQo&T1!IZ83L1tlFtrjLbzqicv&ua^4tbE7F$il*?Gpr!v_N&xm zyr{Yk9#xX!yUV*qZ||GcqXNarh8Y09HzK^i5|klg_AYA;2VzmlIAZmT-xv9^Utw}g zSVUMpgsWy{WWlt7rMYBF97MdPc@b}ua8wyQYF?~;Hco;b&BwX9GdDKPEuSVy^-F6j zbhK_tnAN_hbn>h+-7;M;Bn2xo{=A$lCG}~xYanM?wBfs;PXOJBGrqB!cPp;*=eJcs zOU_q{Ovd8qvqN-FpRf*e8(~M;;Rg>nBK|b}@IDBtKRietJ<}N@`*_KzHC`oTCf{3H zz&^K_r86gZ{!5duRlZP0Mj4ShO9MUP{e@pmX}<*g!?L)R0Na_|m>LOzVabJ<>LUKT zIb)&4=l3D5AH?e4DCt%N`mrj_F``=LjIH7U6(r+_;0<%!j-BaBnj4S70@Q|~Og*3j zlUhf{#0pKFW~brs2iXnOB|)o^1;{_Ir%jgXJN*%nfYO%cAUyM+mUBKe0RZycv+FLeNg0=;5GH^l3s=dd}*oB<$dBAqGZwVH65SG@`dIf_QN0f)u0 zk#MrG4C(G+vYD2-CE=*J>Qd07R7_#}_v+aT7!I#r?HX8CQJ>F= zr4tv+w2+XCGsJpE&1oIw-X;~uUgzB-t(I3vc4vt(e6Xs+DQ+}BOSsWSTY&!h{IR@L zFcjND+$#2lERB3p$=m{Yac2a|#cyHXZOPmj`qzW3;OE>Mps88_!M?tj;+3P{v+}d@ zI-&CmzB6f|M8LtBzcLyd4E`9od@c^HYYW0~FkZxdjD;o)Ke`Q>q=PoEV8`KD5bpYJ z))dR(fDr#BxET6$L`Aq``-O6sr>bA<3Rk~XDrt?%nu=3mzNZ}fvW$HHeS#>ZLaU%E z@jj6i=>m{o5e!51MJNTQ|n51N@&%-vZN<+@9HCSfC2K?(sWb z<2wb%<(OIEdj`|@rW+m+Pp_=;)wrpiAFi{tuYjY@&gf0^or;x960Pz*i;L$zS`~jO z0%=I7OQ=a`45&5~e<=r3O@~Qjhll1i;!oKpVln4x27!w?O=7q6dpPNxKR2G@w;_o8?7DghUYwud z=YTgBonyjZY$mUElTe(4nZh_m{DMh=PI~n{;tXX_X~`dNI>xZq8D|ibI``Vj??g_Q zn`~jV7>eDGi^Dk-1?u;E>PvB1KDLi^a6dZE7G`n!c)KDm1icV?x`Y=PmKKG$VtmT; zi;k7tX{Hb~dt6=(k49}E@Qfi26}KTW5F1{Jfj+&(0bL+kf%B3d z>n?(>hKs|0B{zH0SmeVe-zaH0$8YC$Cp8pO26hx<$qNtSK4IbscMQdy41AbJA|iYR(#z>qBEbU(_VhPyC~hq@6qT5pCpe^( zt5-E7$jEwYuuJM~oTd7@K zC_~1he~J+-S##4DKJHarUHpp0R%q^7`3&aZOzv1G{8EVwV?E;{>-EvSsehSo^MRN(oY#-KGU z@I&SnJOi}O3#;j`8{?T>nPucB>^lyLOfcVLCEF~tI{GL)c@g0y5gkm-r8jG*aRk;A1R|2&*Y@V?Ggh`=fVGyc9mUNb;R>XCBsui3Ci5wA868 zspRvD+{2&fi`_}jBp=Ur&|VNuxl`LcjTtSvWd#wn%jS+w%!Tnd(#Ar+4Ot5(B6_5U zNFXL}QYtvoUqVa^E{=23t`nPM51;-olp5({Q_A+m7@Z#g25 z+QzR&!TXna)^^M)H>xO{VyPqvwu5*5V(bD&Ra1F-;>Xx1c&Po-c zU8C$57&;x^(0(#iBu8d7G`v(D7^Zg8xj0-+cCKk-svsOdleJSoKStdzk zzY3&X|DYJ0U8jzEF=iZ=$QJR@xc&2qey4)&4`>(BoEZ~S`?IjA;ScczslDTCdlUPP zS9rBiJgIq9b3HkvGz^N{&K5qH;rXp;76MtpBE<;#crT1DhXZ z-^wN$RWR4ge`o2lMMf_%j+ImQ>q)nJYRvAnb$dI_LrmWAwspk}dDtz`2)CkGgzvD9 z7{P`2lsuPVe?}L!;Zx4k`6fScaM0fR2Li0?C))~x5bl0pYcq^$L?ZDuaomqN-46>z zF(x`S?P;}LTAK_=Y!TXo4XnAxSTGs7{c^F$AOcp7>e*vNLbJ$Z*3TP0F>i99-^|2$ zJl<8Vg`lqR7k?nx-{`6;m0vtz8=5CqeErM}t~cx<0dWLB|d1o^23(O^0gfK&s{K=rL9M!&J5Ow_z-x#&lQ=ca>FIVoH~bp!0Rvc<_#X)3dcJpv$+s6(Mk^x6{2qwovb0s56?x{Ja`y zU|ro{Xy{9BUm{mf?NGgl|0p;WLTdWs)FBaz!Sn%bUhYG$;I8ZuMmJ_ydXIhcr4I7t z!$*euh_H4`Q}(cY?J_HN68i1g7w{*eo zEPgxhb%K=*c2~YK#H!AsUHQ`CRS~^18ucES$f#LTB=*Q@RFU=pT1x+qnUtB+BH$3= z8GQUVTFf~qn>lcN@>e`ADK>!8IHJ5)%!BIe%?`WyjUnOv{)>F^&;=hwa@GSh3eaog%g@9dlYq$<_)RjKs=%e#ggzknf+F$`#M!6 zo~p8D4La)EuS#=AlzR8YqJ`s`JhZmEb&m(*ZRds}&$zXmr2ii?dw_p%d@Mnbs$n`c zJJ9bF6Ebf4|AuIrxEPx{kqMm$Uf4ExLDelkg)?KxOhI& zlYMY8bOL#jDTzt^*CEp#M32NG>SSsNsjnxaQ#Ex5iJMr0AWHwkAX>x>K@fXlyzH#( z9IOC#PBtJbCp$o!l@-!yjTRy&Z*TH{7x^EB#_mq0W{|pVASV**|2fDwIXEEH1~an% zjj;oNH$eJ*klFoj3;J%KyQ*xgiDFf64(MVt?CZ zW#{}&fB0J;H-zx<2gb$*_}#Yt4~&(Y=QmsA4~&iN-_HgKCo6=}@s}LOZ}!Ze7?6$a zZ=cyXIk^5=3uOP@8UN3@eD?d?Hi z{}F*8d@gA_GkdcCwxr(|Vh}P7zZokJ575vEXv${BX2fk~%+AVX%EfBR3CV&9w=pZL gAkzO^gzP^uhBFAVIsLaw03qQ3B2iO|D@q{!FDx<+%>V!Z 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 09/20] 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: From 75b0db2198ad544eb7504db68fb9b815e7c8d841 Mon Sep 17 00:00:00 2001 From: obinnascale3 <109410793+obinnascale3@users.noreply.github.com> Date: Wed, 2 Apr 2025 05:49:22 +0100 Subject: [PATCH 10/20] fix genai and pinecone instrumentations (#512) * fix genai and pinecone instrumentations * bump version * fix pinecone Index attribute --------- Co-authored-by: Obinna Okafor --- .../instrumentation/google_genai/instrumentation.py | 2 +- src/langtrace_python_sdk/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/langtrace_python_sdk/instrumentation/google_genai/instrumentation.py b/src/langtrace_python_sdk/instrumentation/google_genai/instrumentation.py index eeb92f47..9525f7f4 100644 --- a/src/langtrace_python_sdk/instrumentation/google_genai/instrumentation.py +++ b/src/langtrace_python_sdk/instrumentation/google_genai/instrumentation.py @@ -8,7 +8,7 @@ class GoogleGenaiInstrumentation(BaseInstrumentor): def instrumentation_dependencies(self) -> Collection[str]: - return ["google-genai >= 0.1.0", "google-generativeai < 1.0.0"] + return ["google-genai >= 0.1.0"] def _instrument(self, **kwargs): trace_provider = kwargs.get("tracer_provider") 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 7b0810dad4ff522badf62d8e063515e8c7134961 Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman <105607645+karthikscale3@users.noreply.github.com> Date: Tue, 1 Apr 2025 21:49:32 -0700 Subject: [PATCH 11/20] Improve Agno instrumentation (#513) --- .../instrumentation/agno/instrumentation.py | 23 +- .../instrumentation/agno/patch.py | 224 +++++++++++++----- .../instrumentation/weaviate/patch.py | 8 +- src/langtrace_python_sdk/utils/llm.py | 24 +- 4 files changed, 206 insertions(+), 73 deletions(-) diff --git a/src/langtrace_python_sdk/instrumentation/agno/instrumentation.py b/src/langtrace_python_sdk/instrumentation/agno/instrumentation.py index 5c5df715..11a8c14c 100644 --- a/src/langtrace_python_sdk/instrumentation/agno/instrumentation.py +++ b/src/langtrace_python_sdk/instrumentation/agno/instrumentation.py @@ -21,7 +21,7 @@ from opentelemetry.trace import get_tracer from wrapt import wrap_function_wrapper as _W -from .patch import patch_agent, patch_memory +from .patch import patch_agent, patch_memory, patch_team class AgnoInstrumentation(BaseInstrumentor): @@ -76,6 +76,27 @@ def _instrument(self, **kwargs): patch_memory("AgentMemory.aupdate_summary", version, tracer), ) + _W( + "agno.team.team", + "Team.run", + patch_team("Team.run", version, tracer), + ) + _W( + "agno.team.team", + "Team.arun", + patch_team("Team.arun", version, tracer), + ) + _W( + "agno.team.team", + "Team._run", + patch_team("Team._run", version, tracer), + ) + _W( + "agno.team.team", + "Team._arun", + patch_team("Team._arun", version, tracer), + ) + except Exception: pass diff --git a/src/langtrace_python_sdk/instrumentation/agno/patch.py b/src/langtrace_python_sdk/instrumentation/agno/patch.py index b2a1b59f..cfd8de3a 100644 --- a/src/langtrace_python_sdk/instrumentation/agno/patch.py +++ b/src/langtrace_python_sdk/instrumentation/agno/patch.py @@ -26,9 +26,7 @@ from langtrace_python_sdk.constants import LANGTRACE_SDK_NAME from langtrace_python_sdk.constants.instrumentation.common import ( - LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY, - SERVICE_PROVIDERS, -) + LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY, SERVICE_PROVIDERS) from langtrace_python_sdk.utils import set_span_attribute from langtrace_python_sdk.utils.llm import get_span_name, set_span_attributes from langtrace_python_sdk.utils.misc import serialize_args, serialize_kwargs @@ -62,9 +60,9 @@ def traced_method(wrapped, instance, args, kwargs): try: set_span_attributes(span, attributes) AgnoSpanAttributes(span=span, instance=instance) - + result = wrapped(*args, **kwargs) - + span.set_status(Status(StatusCode.OK)) if operation_name in ["Agent._run", "Agent._arun", "Agent.run", "Agent.arun", "Agent.print_response"]: @@ -72,14 +70,14 @@ def traced_method(wrapped, instance, args, kwargs): if hasattr(instance, "run_response") and instance.run_response: if hasattr(instance.run_response, "run_id") and instance.run_response.run_id: set_span_attribute(span, "agno.agent.run_id", instance.run_response.run_id) - + if hasattr(instance.run_response, "created_at") and instance.run_response.created_at: set_span_attribute(span, "agno.agent.timestamp", instance.run_response.created_at) if hasattr(instance.run_response, "content") and instance.run_response.content: content = str(instance.run_response.content) set_span_attribute(span, "agno.agent.response_content", content) - + # Capture any tools that were used if hasattr(instance.run_response, "tools") and instance.run_response.tools: tools = instance.run_response.tools @@ -122,7 +120,7 @@ def traced_method(wrapped, instance, args, kwargs): else: set_span_attribute(span, "agno.agent.token_usage.input", metrics['input_tokens']) - + if 'output_tokens' in metrics: if isinstance(metrics['output_tokens'], list) and metrics['output_tokens']: set_span_attribute(span, "agno.agent.token_usage.output", @@ -130,7 +128,7 @@ def traced_method(wrapped, instance, args, kwargs): else: set_span_attribute(span, "agno.agent.token_usage.output", metrics['output_tokens']) - + if 'total_tokens' in metrics: if isinstance(metrics['total_tokens'], list) and metrics['total_tokens']: set_span_attribute(span, "agno.agent.token_usage.total", @@ -140,21 +138,21 @@ def traced_method(wrapped, instance, args, kwargs): metrics['total_tokens']) except Exception as err: set_span_attribute(span, "agno.agent.run_response_error", str(err)) - + return result - + except Exception as err: span.record_exception(err) span.set_status(Status(StatusCode.ERROR, str(err))) raise - + return traced_method def patch_memory(operation_name, version, tracer: Tracer): """ Apply instrumentation patches to AgentMemory class methods. - + Args: operation_name: The name of the operation version: The version of Agno @@ -163,7 +161,7 @@ def patch_memory(operation_name, version, tracer: Tracer): def traced_method(wrapped, instance, args, kwargs): service_provider = SERVICE_PROVIDERS.get("AGNO", "agno") extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY) - + # Collect basic span attributes span_attributes = { "langtrace.sdk.name": "langtrace-python-sdk", @@ -180,7 +178,7 @@ def traced_method(wrapped, instance, args, kwargs): inputs["args"] = serialize_args(*args) if len(kwargs) > 0: inputs["kwargs"] = serialize_kwargs(**kwargs) - + span_attributes["agno.memory.inputs"] = json.dumps(inputs) if hasattr(instance, "messages"): @@ -199,10 +197,10 @@ def traced_method(wrapped, instance, args, kwargs): try: # Set attributes set_span_attributes(span, attributes) - + # Execute the wrapped method result = wrapped(*args, **kwargs) - + # Add memory stats after operation if hasattr(instance, "messages"): set_span_attribute(span, "agno.memory.messages_count_after", len(instance.messages)) @@ -210,37 +208,153 @@ def traced_method(wrapped, instance, args, kwargs): set_span_attribute(span, "agno.memory.runs_count_after", len(instance.runs)) if hasattr(instance, "memories") and instance.memories: set_span_attribute(span, "agno.memory.memories_count_after", len(instance.memories)) - + # Record execution time set_span_attribute(span, "agno.memory.execution_time_ms", int((time.time() - start_time) * 1000)) - + # Record success status span.set_status(Status(StatusCode.OK)) - + # Add result if relevant if result is not None: set_span_attribute(span, "agno.memory.result", str(result)) - + return result - + except Exception as err: # Record the exception span.record_exception(err) span.set_status(Status(StatusCode.ERROR, str(err))) raise - + return traced_method +def patch_team(operation_name, version, tracer: Tracer): + def traced_method(wrapped, instance, args, kwargs): + service_provider = SERVICE_PROVIDERS.get("AGNO", "agno") + 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": "framework", + "langtrace.service.version": version, + "langtrace.version": v(LANGTRACE_SDK_NAME), + **(extra_attributes if extra_attributes is not None else {}), + } + + inputs = {} + if len(args) > 0: + inputs["args"] = serialize_args(*args) + if len(kwargs) > 0: + inputs["kwargs"] = serialize_kwargs(**kwargs) + span_attributes["agno.team.inputs"] = json.dumps(inputs) + attributes = FrameworkSpanAttributes(**span_attributes) + + with tracer.start_as_current_span( + get_span_name(operation_name), kind=SpanKind.CLIENT + ) as span: + try: + set_span_attributes(span, attributes) + AgnoSpanAttributes(span=span, instance=instance) + + result = wrapped(*args, **kwargs) + + span.set_status(Status(StatusCode.OK)) + + if operation_name in ["Team.run", "Team.arun", "Team._run", "Team._arun"]: + try: + if hasattr(instance, "run_response") and instance.run_response: + if hasattr(instance.run_response, "run_id") and instance.run_response.run_id: + set_span_attribute(span, "agno.team.run_id", instance.run_response.run_id) + + if hasattr(instance.run_response, "created_at") and instance.run_response.created_at: + set_span_attribute(span, "agno.team.timestamp", instance.run_response.created_at) + + if hasattr(instance.run_response, "content") and instance.run_response.content: + content = str(instance.run_response.content) + set_span_attribute(span, "agno.team.response_content", content) + + # Capture any tools that were used + if hasattr(instance.run_response, "tools") and instance.run_response.tools: + tools = instance.run_response.tools + tool_summary = [] + for tool in tools: + if 'tool_name' in tool: + tool_summary.append(tool['tool_name']) + elif 'function' in tool and 'name' in tool['function']: + tool_summary.append(tool['function']['name']) + set_span_attribute(span, "agno.team.tools_used", json.dumps(tool_summary)) + + if hasattr(instance.run_response, "metrics") and instance.run_response.metrics: + metrics = instance.run_response.metrics + for metric_name, metric_values in metrics.items(): + if isinstance(metric_values, list): + + if all(isinstance(v, (int, float)) for v in metric_values): + set_span_attribute( + span, + f"agno.team.metrics.{metric_name}", + sum(metric_values) / len(metric_values) if metric_values else 0 + ) + elif len(metric_values) > 0: + set_span_attribute( + span, + f"agno.team.metrics.{metric_name}", + str(metric_values[-1]) + ) + else: + set_span_attribute( + span, + f"agno.team.metrics.{metric_name}", + str(metric_values) + ) + + if 'input_tokens' in metrics: + if isinstance(metrics['input_tokens'], list) and metrics['input_tokens']: + set_span_attribute(span, "agno.team.token_usage.input", + sum(metrics['input_tokens'])) + else: + set_span_attribute(span, "agno.team.token_usage.input", + metrics['input_tokens']) + + if 'output_tokens' in metrics: + if isinstance(metrics['output_tokens'], list) and metrics['output_tokens']: + set_span_attribute(span, "agno.team.token_usage.output", + sum(metrics['output_tokens'])) + else: + set_span_attribute(span, "agno.team.token_usage.output", + metrics['output_tokens']) + + if 'total_tokens' in metrics: + if isinstance(metrics['total_tokens'], list) and metrics['total_tokens']: + set_span_attribute(span, "agno.team.token_usage.total", + sum(metrics['total_tokens'])) + else: + set_span_attribute(span, "agno.team.token_usage.total", + metrics['total_tokens']) + except Exception as err: + set_span_attribute(span, "agno.team.run_response_error", str(err)) + + return result + + except Exception as err: + span.record_exception(err) + span.set_status(Status(StatusCode.ERROR, str(err))) + raise + + return traced_method + class AgnoSpanAttributes: """ Helper class to extract and set Agno Agent attributes on spans. """ - + def __init__(self, span: Span, instance: Any) -> None: """ Initialize with a span and Agno instance. - + Args: span: OpenTelemetry span to update instance: Agno Agent instance @@ -248,14 +362,14 @@ def __init__(self, span: Span, instance: Any) -> None: self.span = span self.instance = instance self.agent_data = {} - + self.run() - + def run(self) -> None: """Process the instance attributes and add them to the span.""" # Collect basic agent attributes self.collect_agent_attributes() - + # Add attributes to span for key, value in self.agent_data.items(): if value is not None: @@ -264,68 +378,68 @@ def run(self) -> None: f"agno.agent.{key}", str(value) if not isinstance(value, (int, float, bool)) else value ) - + def collect_agent_attributes(self) -> None: """Collect important attributes from the Agent instance.""" # Extract basic agent information if hasattr(self.instance, "agent_id"): self.agent_data["id"] = self.instance.agent_id - + if hasattr(self.instance, "name"): self.agent_data["name"] = self.instance.name - + if hasattr(self.instance, "session_id"): self.agent_data["session_id"] = self.instance.session_id - + if hasattr(self.instance, "user_id"): self.agent_data["user_id"] = self.instance.user_id - + if hasattr(self.instance, "run_id"): self.agent_data["run_id"] = self.instance.run_id - - # Extract model information + + # Extract model information if hasattr(self.instance, "model") and self.instance.model: model = self.instance.model model_info = {} - + if hasattr(model, "id"): model_info["id"] = model.id - + if hasattr(model, "name"): model_info["name"] = model.name - + if hasattr(model, "provider"): model_info["provider"] = model.provider - + # Add temperature if available if hasattr(model, "temperature") and model.temperature is not None: model_info["temperature"] = model.temperature - + # Add max_tokens if available if hasattr(model, "max_tokens") and model.max_tokens is not None: model_info["max_tokens"] = model.max_tokens - + self.agent_data["model"] = json.dumps(model_info) - + # Extract tool information if hasattr(self.instance, "tools") and self.instance.tools: tool_info = [] for tool in self.instance.tools: tool_data = {} - + # Handle different types of tools if hasattr(tool, "name"): tool_data["name"] = tool.name - + # Handle DuckDuckGoTools and similar toolkits if hasattr(tool, "functions") and isinstance(tool.functions, dict): tool_data["functions"] = list(tool.functions.keys()) - + elif hasattr(tool, "__name__"): tool_data["name"] = tool.__name__ else: tool_data["name"] = str(tool) - + # Add functions if available if not "functions" in tool_data and hasattr(tool, "functions"): if callable(getattr(tool, "functions")): @@ -336,28 +450,28 @@ def collect_agent_attributes(self) -> None: for f in tool_functions] except: pass - + tool_info.append(tool_data) - + self.agent_data["tools"] = json.dumps(tool_info) - + # Extract reasoning settings if hasattr(self.instance, "reasoning") and self.instance.reasoning: self.agent_data["reasoning_enabled"] = True - + if hasattr(self.instance, "reasoning_model") and self.instance.reasoning_model: self.agent_data["reasoning_model"] = str(self.instance.reasoning_model.id) - + if hasattr(self.instance, "reasoning_min_steps"): self.agent_data["reasoning_min_steps"] = self.instance.reasoning_min_steps - + if hasattr(self.instance, "reasoning_max_steps"): self.agent_data["reasoning_max_steps"] = self.instance.reasoning_max_steps - + # Extract knowledge settings if hasattr(self.instance, "knowledge") and self.instance.knowledge: self.agent_data["knowledge_enabled"] = True - + # Extract streaming settings if hasattr(self.instance, "stream"): - self.agent_data["stream"] = self.instance.stream \ No newline at end of file + self.agent_data["stream"] = self.instance.stream diff --git a/src/langtrace_python_sdk/instrumentation/weaviate/patch.py b/src/langtrace_python_sdk/instrumentation/weaviate/patch.py index 6ed88b07..f18c00b9 100644 --- a/src/langtrace_python_sdk/instrumentation/weaviate/patch.py +++ b/src/langtrace_python_sdk/instrumentation/weaviate/patch.py @@ -26,9 +26,7 @@ from langtrace_python_sdk.constants import LANGTRACE_SDK_NAME from langtrace_python_sdk.constants.instrumentation.common import ( - LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY, - SERVICE_PROVIDERS, -) + LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY, SERVICE_PROVIDERS) from langtrace_python_sdk.constants.instrumentation.weaviate import APIS from langtrace_python_sdk.utils.llm import get_span_name from langtrace_python_sdk.utils.misc import extract_input_params, to_iso_format @@ -53,6 +51,8 @@ def extract_inputs(args, kwargs): for k, v in kwargs.items() if k not in ["properties", "fusion_type", "filters"] } + # don't include vector in kwargs_without_properties + kwargs_without_properties.pop("vector", None) extracted_params.update(extract_input_params(args, kwargs_without_properties)) if kwargs.get("filters", None): extracted_params["filters"] = str(kwargs["filters"]) @@ -109,7 +109,7 @@ def convert_value(value): **{k: convert_value(v) for k, v in response_object.properties.items()}, "uuid": str(response_object.uuid) if hasattr(response_object, "uuid") else None, "collection": getattr(response_object, "collection", None), - "vector": getattr(response_object, "vector", None), + # "vector": getattr(response_object, "vector", None), "references": getattr(response_object, "references", None), "metadata": ( extract_metadata(response_object.metadata) diff --git a/src/langtrace_python_sdk/utils/llm.py b/src/langtrace_python_sdk/utils/llm.py index d35e618d..d2dcfb7f 100644 --- a/src/langtrace_python_sdk/utils/llm.py +++ b/src/langtrace_python_sdk/utils/llm.py @@ -14,25 +14,24 @@ limitations under the License. """ +import json +import os from typing import Any, Dict, Union -from langtrace_python_sdk.constants import LANGTRACE_SDK_NAME -from langtrace_python_sdk.utils import set_span_attribute -from langtrace_python_sdk.types import NOT_GIVEN -from tiktoken import get_encoding, list_encoding_names -from langtrace_python_sdk.constants.instrumentation.common import ( - LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY, - TIKTOKEN_MODEL_MAPPING, -) -from langtrace_python_sdk.constants.instrumentation.openai import OPENAI_COST_TABLE -from langtrace.trace_attributes import SpanAttributes from importlib_metadata import version as v -import json +from langtrace.trace_attributes import SpanAttributes from opentelemetry import baggage from opentelemetry.trace import Span from opentelemetry.trace.status import StatusCode +from tiktoken import get_encoding, list_encoding_names -import os +from langtrace_python_sdk.constants import LANGTRACE_SDK_NAME +from langtrace_python_sdk.constants.instrumentation.common import ( + LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY, TIKTOKEN_MODEL_MAPPING) +from langtrace_python_sdk.constants.instrumentation.openai import \ + OPENAI_COST_TABLE +from langtrace_python_sdk.types import NOT_GIVEN +from langtrace_python_sdk.utils import set_span_attribute def get_span_name(operation_name): @@ -437,7 +436,6 @@ def cleanup(self): "".join(self.result_content), response_model ) if self._span_started: - print("SPAAN", self.span) set_span_attribute( self.span, SpanAttributes.LLM_RESPONSE_MODEL, From ae3598b7af0f56e72f5802a77b89014c23cf2a34 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Wed, 2 Apr 2025 12:56:53 +0300 Subject: [PATCH 12/20] cast token count to int --- .../instrumentation/cohere/patch.py | 121 +++++++++--------- src/langtrace_python_sdk/utils/llm.py | 6 +- 2 files changed, 65 insertions(+), 62 deletions(-) diff --git a/src/langtrace_python_sdk/instrumentation/cohere/patch.py b/src/langtrace_python_sdk/instrumentation/cohere/patch.py index 8b9a8e53..524b14d6 100644 --- a/src/langtrace_python_sdk/instrumentation/cohere/patch.py +++ b/src/langtrace_python_sdk/instrumentation/cohere/patch.py @@ -87,25 +87,25 @@ def traced_method(wrapped, instance, args, kwargs): and result.meta.billed_units is not None ): usage = result.meta.billed_units - if usage is not None: - span.set_attribute( - SpanAttributes.LLM_USAGE_PROMPT_TOKENS, - usage.input_tokens or 0, - ) - span.set_attribute( - SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, - usage.output_tokens or 0, - ) - - span.set_attribute( - SpanAttributes.LLM_USAGE_TOTAL_TOKENS, - (usage.input_tokens or 0) + (usage.output_tokens or 0), - ) - - span.set_attribute( - "search_units", - usage.search_units or 0, - ) + input_tokens = int(usage.input_tokens) if usage.input_tokens else 0 + output_tokens = int(usage.output_tokens) if usage.output_tokens else 0 + span.set_attribute( + SpanAttributes.LLM_USAGE_PROMPT_TOKENS, + input_tokens, + ) + span.set_attribute( + SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, + output_tokens, + ) + span.set_attribute( + SpanAttributes.LLM_USAGE_TOTAL_TOKENS, + input_tokens + output_tokens, + ) + span.set_attribute( + "search_units", + int(usage.search_units) if usage.search_units else 0, + ) + span.set_status(StatusCode.OK) span.end() @@ -309,25 +309,25 @@ def traced_method(wrapped, instance, args, kwargs): and result.meta.billed_units is not None ): usage = result.meta.billed_units - if usage is not None: - span.set_attribute( - SpanAttributes.LLM_USAGE_PROMPT_TOKENS, - usage.input_tokens or 0, - ) - span.set_attribute( - SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, - usage.output_tokens or 0, - ) - - span.set_attribute( - SpanAttributes.LLM_USAGE_TOTAL_TOKENS, - (usage.input_tokens or 0) + (usage.output_tokens or 0), - ) - - span.set_attribute( - "search_units", - usage.search_units or 0, - ) + input_tokens = int(usage.input_tokens) if usage.input_tokens else 0 + output_tokens = int(usage.output_tokens) if usage.output_tokens else 0 + span.set_attribute( + SpanAttributes.LLM_USAGE_PROMPT_TOKENS, + input_tokens, + ) + span.set_attribute( + SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, + output_tokens, + ) + span.set_attribute( + SpanAttributes.LLM_USAGE_TOTAL_TOKENS, + input_tokens + output_tokens, + ) + span.set_attribute( + "search_units", + int(usage.search_units) if usage.search_units else 0, + ) + span.set_status(StatusCode.OK) span.end() return result @@ -419,10 +419,12 @@ def traced_method(wrapped, instance, args, kwargs): if (hasattr(result.usage, "billed_units") and result.usage.billed_units is not None): usage = result.usage.billed_units + input_tokens = int(usage.input_tokens) if usage.input_tokens else 0 + output_tokens = int(usage.output_tokens) if usage.output_tokens else 0 for metric, value in { - "input": usage.input_tokens or 0, - "output": usage.output_tokens or 0, - "total": (usage.input_tokens or 0) + (usage.output_tokens or 0), + "input": input_tokens, + "output": output_tokens, + "total": input_tokens + output_tokens, }.items(): span.set_attribute( f"gen_ai.usage.{metric}_tokens", @@ -571,26 +573,27 @@ def traced_method(wrapped, instance, args, kwargs): and response.meta.billed_units is not None ): usage = response.meta.billed_units - if usage is not None: - span.set_attribute( - SpanAttributes.LLM_USAGE_PROMPT_TOKENS, - usage.input_tokens or 0, - ) - span.set_attribute( - SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, - usage.output_tokens or 0, - ) - + input_tokens = int(usage.input_tokens) if usage.input_tokens else 0 + output_tokens = int(usage.output_tokens) if usage.output_tokens else 0 + span.set_attribute( + SpanAttributes.LLM_USAGE_PROMPT_TOKENS, + input_tokens, + ) + span.set_attribute( + SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, + output_tokens, + ) + span.set_attribute( + SpanAttributes.LLM_USAGE_TOTAL_TOKENS, + input_tokens + output_tokens, + ) + + if usage.search_units is not None: span.set_attribute( - SpanAttributes.LLM_USAGE_TOTAL_TOKENS, - (usage.input_tokens or 0) - + (usage.output_tokens or 0), + "search_units", + int(usage.search_units) if usage.search_units else 0, ) - if usage.search_units is not None: - span.set_attribute( - "search_units", - usage.search_units or 0, - ) + yield event finally: diff --git a/src/langtrace_python_sdk/utils/llm.py b/src/langtrace_python_sdk/utils/llm.py index d2dcfb7f..f2261405 100644 --- a/src/langtrace_python_sdk/utils/llm.py +++ b/src/langtrace_python_sdk/utils/llm.py @@ -347,19 +347,19 @@ def set_usage_attributes(span, usage): set_span_attribute( span, SpanAttributes.LLM_USAGE_PROMPT_TOKENS, - input_tokens, + int(input_tokens), ) set_span_attribute( span, SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, - output_tokens, + int(output_tokens), ) set_span_attribute( span, SpanAttributes.LLM_USAGE_TOTAL_TOKENS, - input_tokens + output_tokens, + int(input_tokens) + int(output_tokens), ) if "search_units" in usage: From f83afdae6bd632905e4ca6e519705b1248feb3d5 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Wed, 2 Apr 2025 13:00:21 +0300 Subject: [PATCH 13/20] bump 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 01d6f588..b01d1044 100644 --- a/src/langtrace_python_sdk/version.py +++ b/src/langtrace_python_sdk/version.py @@ -1 +1 @@ -__version__ = "3.8.10" +__version__ = "3.8.11" From 528c3d8b46ca166c141c72ceb6d3a3ec47985591 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Wed, 2 Apr 2025 15:08:43 +0300 Subject: [PATCH 14/20] only redirect stdout if logging is disabled --- src/langtrace_python_sdk/langtrace.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/langtrace_python_sdk/langtrace.py b/src/langtrace_python_sdk/langtrace.py index d01aeae4..9d978f31 100644 --- a/src/langtrace_python_sdk/langtrace.py +++ b/src/langtrace_python_sdk/langtrace.py @@ -299,7 +299,8 @@ def init( init_instrumentations(config.disable_instrumentations, all_instrumentations) add_span_processor(provider, config, exporter) - sys.stdout = sys.__stdout__ + if config.disable_logging: + sys.stdout = sys.__stdout__ init_sentry(config, host) From 624cdc3cd1a69beba9d68b29b8d33642e6f52680 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Wed, 2 Apr 2025 15:10:23 +0300 Subject: [PATCH 15/20] bump 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 01d6f588..b01d1044 100644 --- a/src/langtrace_python_sdk/version.py +++ b/src/langtrace_python_sdk/version.py @@ -1 +1 @@ -__version__ = "3.8.10" +__version__ = "3.8.11" From 56e78cb75c61f1bae863964e4a3829d7dd950993 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Wed, 2 Apr 2025 15:35:00 +0300 Subject: [PATCH 16/20] fix openai agents attribute error --- src/langtrace_python_sdk/instrumentation/openai/patch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/langtrace_python_sdk/instrumentation/openai/patch.py b/src/langtrace_python_sdk/instrumentation/openai/patch.py index 5d642a71..3718e149 100644 --- a/src/langtrace_python_sdk/instrumentation/openai/patch.py +++ b/src/langtrace_python_sdk/instrumentation/openai/patch.py @@ -738,7 +738,7 @@ def _set_openai_agentic_response_attributes(span: Span, response) -> None: "input_tokens": response.usage.input_tokens, "output_tokens": response.usage.output_tokens, "total_tokens": response.usage.total_tokens, - "cached_tokens": response.usage.input_tokens_details["cached_tokens"], + "cached_tokens": response.usage.input_tokens_details.cached_tokens, }, ) From 519cfdb34cddc3dbef35a93d98e7038439462bda Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Wed, 2 Apr 2025 15:35:21 +0300 Subject: [PATCH 17/20] bump 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 01d6f588..b01d1044 100644 --- a/src/langtrace_python_sdk/version.py +++ b/src/langtrace_python_sdk/version.py @@ -1 +1 @@ -__version__ = "3.8.10" +__version__ = "3.8.11" From 3777dd1cdc2ba93c927e95846e11c5d7c88c1b52 Mon Sep 17 00:00:00 2001 From: obinnascale3 <109410793+obinnascale3@users.noreply.github.com> Date: Wed, 2 Apr 2025 22:29:01 +0100 Subject: [PATCH 18/20] fix openai responses api instrumentation (#519) Co-authored-by: Obinna Okafor --- src/langtrace_python_sdk/instrumentation/openai/patch.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/langtrace_python_sdk/instrumentation/openai/patch.py b/src/langtrace_python_sdk/instrumentation/openai/patch.py index 3718e149..ed2592ad 100644 --- a/src/langtrace_python_sdk/instrumentation/openai/patch.py +++ b/src/langtrace_python_sdk/instrumentation/openai/patch.py @@ -114,6 +114,9 @@ def traced_method( return StreamWrapper(response, span) else: _set_openai_agentic_response_attributes(span, response) + + span.set_status(StatusCode.OK) + span.end() return response except Exception as err: span.record_exception(err) From 2884436905cae1635f210dc8a5f0c2f850ce3a44 Mon Sep 17 00:00:00 2001 From: obinnascale3 <109410793+obinnascale3@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:21:42 +0100 Subject: [PATCH 19/20] fix JSON serializable error in neo4j (#521) Co-authored-by: Obinna Okafor --- src/langtrace_python_sdk/instrumentation/neo4j/patch.py | 8 ++++++-- src/langtrace_python_sdk/version.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/langtrace_python_sdk/instrumentation/neo4j/patch.py b/src/langtrace_python_sdk/instrumentation/neo4j/patch.py index c3633472..c271f0da 100644 --- a/src/langtrace_python_sdk/instrumentation/neo4j/patch.py +++ b/src/langtrace_python_sdk/instrumentation/neo4j/patch.py @@ -37,7 +37,11 @@ def driver_patch(operation_name, version, tracer): def traced_method(wrapped, instance, args, kwargs): - + try: + query = args[0].text if hasattr(args[0], "text") else args[0] + query_text = json.dumps(query) + except (AttributeError, TypeError): + query_text = args[0] api = APIS[operation_name] service_provider = SERVICE_PROVIDERS.get("NEO4J", "neo4j") extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY) @@ -49,7 +53,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(args[0]) if args and len(args) > 0 else "", + "db.query": query_text, **(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 b01d1044..c70e209f 100644 --- a/src/langtrace_python_sdk/version.py +++ b/src/langtrace_python_sdk/version.py @@ -1 +1 @@ -__version__ = "3.8.11" +__version__ = "3.8.12" From 0e522e125ec903fe326bb724becb6ba97d301cc0 Mon Sep 17 00:00:00 2001 From: Obinna Okafor Date: Tue, 8 Apr 2025 20:36:34 +0300 Subject: [PATCH 20/20] fix merge conflict --- src/langtrace_python_sdk/instrumentation/neo4j/patch.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/langtrace_python_sdk/instrumentation/neo4j/patch.py b/src/langtrace_python_sdk/instrumentation/neo4j/patch.py index 0bbd9f77..c271f0da 100644 --- a/src/langtrace_python_sdk/instrumentation/neo4j/patch.py +++ b/src/langtrace_python_sdk/instrumentation/neo4j/patch.py @@ -53,11 +53,7 @@ def traced_method(wrapped, instance, args, kwargs): "langtrace.version": v(LANGTRACE_SDK_NAME), "db.system": "neo4j", "db.operation": api["OPERATION"], -<<<<<<< HEAD - "db.query": json.dumps(args[0]) if args and len(args) > 0 else "", -======= "db.query": query_text, ->>>>>>> development **(extra_attributes if extra_attributes is not None else {}), }