diff --git a/.gitignore b/.gitignore index 2e63824d04..ec2a9fe8ca 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ npm-debug.log yarn-error.log testem.log /typings +.tool-versions # System Files .DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc520dc2c7..8cde353bd5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,3 +4,39 @@ Thanks for taking the time to contribute! 😃 🚀 Please refer to our [Contributing Guide](https://traceloop.com/docs/openllmetry/contributing/overview) for instructions on how to contribute. +## Local Testing and Linting in this Repo + +A few steps to set up this repo locally. + +Run the following at repo root to setup the yarn dependencies. +```shell +npm ci +``` + +Install `poetry` (version 2) at the system-level (e.g. using `pipx` or `brew`). + +`poetry` does not recognize the `.python-version` specification in python projects. +It has to be told explicitly what version to use. +Otherwise, it may attempt to use a newer, unsupported python version and encounter build errors. + +```shell +cd packages/opentelemetry-instrumentation-openai/ +poetry python install $(head -n1 .python-version) +poetry env use $(head -n1 .python-version) +poetry install +``` + +Generally, for setting up and testing an individual package, run the following from repo root. + +```shell +npx nx run opentelemetry-instrumentation-openai:install --with dev,test +npx nx run opentelemetry-instrumentation-openai:lint +npx nx run opentelemetry-instrumentation-openai:test +``` + +Or you can run the following to automatically set up all affected packages. +```shell +npx nx affected -t install --with dev,test +npx nx affected -t lint +npx nx affected -t test +``` diff --git a/packages/opentelemetry-instrumentation-openai-agents/opentelemetry/instrumentation/openai_agents/_hooks.py b/packages/opentelemetry-instrumentation-openai-agents/opentelemetry/instrumentation/openai_agents/_hooks.py index 995e062bf2..f364c22f4e 100644 --- a/packages/opentelemetry-instrumentation-openai-agents/opentelemetry/instrumentation/openai_agents/_hooks.py +++ b/packages/opentelemetry-instrumentation-openai-agents/opentelemetry/instrumentation/openai_agents/_hooks.py @@ -1,6 +1,6 @@ """Hook-based instrumentation for OpenAI Agents using the SDK's native callback system.""" -from typing import Dict, Any +from typing import Dict, Any, Final import json import time from collections import OrderedDict @@ -11,7 +11,7 @@ gen_ai_attributes as GenAIAttributes ) from agents.tracing.processors import TracingProcessor -from .utils import dont_throw +from .utils import dont_throw, should_send_prompts try: # Attempt to import once, so that we aren't looking for it repeatedly. @@ -239,6 +239,7 @@ def on_span_end(self, span): if span in self._otel_spans: otel_span = self._otel_spans[span] span_data = getattr(span, 'span_data', None) + should_trace_content: Final[bool] = should_send_prompts() if span_data and ( type(span_data).__name__ == 'ResponseSpanData' or isinstance( span_data, @@ -304,7 +305,7 @@ def on_span_end(self, span): otel_span.set_attribute(f"{prefix}.role", role) # Set content attribute - if content is not None: + if should_trace_content and content is not None: if not isinstance(content, str): content = json.dumps(content) otel_span.set_attribute(f"{prefix}.content", content) @@ -405,7 +406,7 @@ def on_span_end(self, span): if hasattr(response, 'output') and response.output: for i, output in enumerate(response.output): # Handle different output types - if hasattr(output, 'content') and output.content: + if should_trace_content and hasattr(output, 'content') and output.content: # Text message with content array (ResponseOutputMessage) content_text = "" for content_item in output.content: @@ -436,7 +437,7 @@ def on_span_end(self, span): otel_span.set_attribute( f"{GenAIAttributes.GEN_AI_COMPLETION}.{i}.tool_calls.0.id", tool_call_id) - elif hasattr(output, 'text'): + elif should_trace_content and hasattr(output, 'text'): # Direct text content otel_span.set_attribute(f"{GenAIAttributes.GEN_AI_COMPLETION}.{i}.content", output.text) otel_span.set_attribute( @@ -472,7 +473,7 @@ def on_span_end(self, span): elif span_data: # Extract prompt data from input and add to response span (legacy support) input_data = getattr(span_data, 'input', []) - if input_data: + if should_trace_content and input_data: for i, message in enumerate(input_data): if hasattr(message, 'role') and hasattr(message, 'content'): otel_span.set_attribute(f"gen_ai.prompt.{i}.role", message.role) @@ -514,7 +515,7 @@ def on_span_end(self, span): if hasattr(response, 'output') and response.output: for i, output in enumerate(response.output): # Handle different output types - if hasattr(output, 'content') and output.content: + if should_trace_content and hasattr(output, 'content') and output.content: # Text message with content array (ResponseOutputMessage) content_text = "" for content_item in output.content: @@ -545,7 +546,7 @@ def on_span_end(self, span): otel_span.set_attribute( f"{GenAIAttributes.GEN_AI_COMPLETION}.{i}.tool_calls.0.id", tool_call_id) - elif hasattr(output, 'text'): + elif should_trace_content and hasattr(output, 'text'): # Direct text content otel_span.set_attribute(f"{GenAIAttributes.GEN_AI_COMPLETION}.{i}.content", output.text) otel_span.set_attribute( diff --git a/packages/opentelemetry-instrumentation-openai-agents/opentelemetry/instrumentation/openai_agents/utils.py b/packages/opentelemetry-instrumentation-openai-agents/opentelemetry/instrumentation/openai_agents/utils.py index e8f6e33a55..d9fb476639 100644 --- a/packages/opentelemetry-instrumentation-openai-agents/opentelemetry/instrumentation/openai_agents/utils.py +++ b/packages/opentelemetry-instrumentation-openai-agents/opentelemetry/instrumentation/openai_agents/utils.py @@ -6,6 +6,8 @@ import traceback from opentelemetry import context as context_api +_TRACELOOP_TRACE_CONTENT = "TRACELOOP_TRACE_CONTENT" + def set_span_attribute(span, name, value): if value is not None: @@ -18,10 +20,14 @@ def _is_truthy(value): return str(value).strip().lower() in ("true", "1", "yes", "on") -def should_send_prompts(): - env_setting = os.getenv("TRACELOOP_TRACE_CONTENT", "true") +def should_send_prompts() -> bool: + """Determine if LLM content tracing should be enabled. + + Content includes not only prompts, but also responses. + """ + env_setting = os.getenv(_TRACELOOP_TRACE_CONTENT, "true") override = context_api.get_value("override_enable_content_tracing") - return _is_truthy(env_setting) or bool(override) + return _is_truthy(env_setting) or _is_truthy(override) class JSONEncoder(json.JSONEncoder): diff --git a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/utils.py b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/utils.py index 1b16fa3fec..1de4e63f3b 100644 --- a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/utils.py +++ b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/utils.py @@ -174,10 +174,18 @@ def run_async(method): asyncio.run(method) -def should_send_prompts(): - return ( - os.getenv(TRACELOOP_TRACE_CONTENT) or "true" - ).lower() == "true" or context_api.get_value("override_enable_content_tracing") +def _is_truthy(value): + return str(value).strip().lower() in ("true", "1", "yes", "on") + + +def should_send_prompts() -> bool: + """Determine if LLM content tracing should be enabled. + + Content includes not only prompts, but also responses. + """ + env_setting = os.getenv(TRACELOOP_TRACE_CONTENT, "true") + override = context_api.get_value("override_enable_content_tracing") + return _is_truthy(env_setting) or _is_truthy(override) def should_emit_events() -> bool: