diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py index 6c7cab7b91..d9bf2d898a 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py @@ -34,7 +34,7 @@ logger = logging.getLogger(__name__) -_instruments = ("langchain-core > 0.1.0", ) +_instruments = ("langchain-core > 0.1.0",) class LangchainInstrumentor(BaseInstrumentor): @@ -111,18 +111,15 @@ def _instrument(self, **kwargs): if not Config.use_legacy_attributes: logger_provider = kwargs.get("logger_provider") - Config.event_logger = get_logger( - __name__, __version__, logger_provider=logger_provider - ) + Config.event_logger = get_logger(__name__, __version__, logger_provider=logger_provider) - traceloopCallbackHandler = TraceloopCallbackHandler( - tracer, duration_histogram, token_histogram - ) + traceloopCallbackHandler = TraceloopCallbackHandler(tracer, duration_histogram, token_histogram) wrap_function_wrapper( "langchain_core.callbacks", "BaseCallbackManager.__init__", - _BaseCallbackManagerInitWrapper(traceloopCallbackHandler, - ), + _BaseCallbackManagerInitWrapper( + traceloopCallbackHandler, + ), ) # Wrap LangGraph components if available @@ -137,67 +134,67 @@ def _wrap_openai_functions_for_tracing(self, traceloopCallbackHandler): if is_package_available("langchain_community"): # Wrap langchain_community.llms.openai.BaseOpenAI wrap_function_wrapper( - "langchain_community.llms.openai", - "BaseOpenAI._generate", - openai_tracing_wrapper, - ) + "langchain_community.llms.openai", + "BaseOpenAI._generate", + openai_tracing_wrapper, + ) wrap_function_wrapper( - "langchain_community.llms.openai", - "BaseOpenAI._agenerate", - openai_tracing_wrapper, - ) + "langchain_community.llms.openai", + "BaseOpenAI._agenerate", + openai_tracing_wrapper, + ) wrap_function_wrapper( - "langchain_community.llms.openai", - "BaseOpenAI._stream", - openai_tracing_wrapper, - ) + "langchain_community.llms.openai", + "BaseOpenAI._stream", + openai_tracing_wrapper, + ) wrap_function_wrapper( - "langchain_community.llms.openai", - "BaseOpenAI._astream", - openai_tracing_wrapper, - ) + "langchain_community.llms.openai", + "BaseOpenAI._astream", + openai_tracing_wrapper, + ) if is_package_available("langchain_openai"): # Wrap langchain_openai.llms.base.BaseOpenAI wrap_function_wrapper( - "langchain_openai.llms.base", - "BaseOpenAI._generate", - openai_tracing_wrapper, - ) + "langchain_openai.llms.base", + "BaseOpenAI._generate", + openai_tracing_wrapper, + ) wrap_function_wrapper( - "langchain_openai.llms.base", - "BaseOpenAI._agenerate", - openai_tracing_wrapper, - ) + "langchain_openai.llms.base", + "BaseOpenAI._agenerate", + openai_tracing_wrapper, + ) wrap_function_wrapper( - "langchain_openai.llms.base", - "BaseOpenAI._stream", - openai_tracing_wrapper, - ) + "langchain_openai.llms.base", + "BaseOpenAI._stream", + openai_tracing_wrapper, + ) wrap_function_wrapper( - "langchain_openai.llms.base", - "BaseOpenAI._astream", - openai_tracing_wrapper, - ) + "langchain_openai.llms.base", + "BaseOpenAI._astream", + openai_tracing_wrapper, + ) # langchain_openai.chat_models.base.BaseOpenAI wrap_function_wrapper( - "langchain_openai.chat_models.base", - "BaseChatOpenAI._generate", - openai_tracing_wrapper, - ) + "langchain_openai.chat_models.base", + "BaseChatOpenAI._generate", + openai_tracing_wrapper, + ) wrap_function_wrapper( - "langchain_openai.chat_models.base", - "BaseChatOpenAI._agenerate", - openai_tracing_wrapper, - ) + "langchain_openai.chat_models.base", + "BaseChatOpenAI._agenerate", + openai_tracing_wrapper, + ) # Doesn't work :( # wrap_function_wrapper( @@ -217,16 +214,20 @@ def _wrap_langgraph_components(self, tracer): if is_package_available("langgraph"): try: wrap_function_wrapper( - "langgraph.pregel", - "Pregel.stream", - create_graph_invocation_wrapper(tracer, is_async=False, - ), + "langgraph.pregel", + "Pregel.stream", + create_graph_invocation_wrapper( + tracer, + is_async=False, + ), ) wrap_function_wrapper( - "langgraph.pregel", - "Pregel.astream", - create_graph_invocation_wrapper(tracer, is_async=True, - ), + "langgraph.pregel", + "Pregel.astream", + create_graph_invocation_wrapper( + tracer, + is_async=True, + ), ) except Exception as e: logger.debug("Failed to wrap Pregel methods: %s", e) @@ -234,10 +235,11 @@ def _wrap_langgraph_components(self, tracer): # Wrap Command.__init__ to capture routing commands try: wrap_function_wrapper( - "langgraph.types", - "Command.__init__", - create_command_init_wrapper(tracer, - ), + "langgraph.types", + "Command.__init__", + create_command_init_wrapper( + tracer, + ), ) except Exception as e: logger.debug("Failed to wrap Command.__init__: %s", e) @@ -257,19 +259,19 @@ def _wrap_agent_factories(self, tracer): # Patch the actual module where the function is defined try: wrap_function_wrapper( - "langgraph.prebuilt.chat_agent_executor", - "create_react_agent", - langgraph_agent_wrapper, - ) + "langgraph.prebuilt.chat_agent_executor", + "create_react_agent", + langgraph_agent_wrapper, + ) except Exception as e: logger.debug("Failed to wrap langgraph.prebuilt.chat_agent_executor.create_react_agent: %s", e) # Also patch the re-export location for imports from langgraph.prebuilt try: wrap_function_wrapper( - "langgraph.prebuilt", - "create_react_agent", - langgraph_agent_wrapper, - ) + "langgraph.prebuilt", + "create_react_agent", + langgraph_agent_wrapper, + ) except Exception as e: logger.debug("Failed to wrap langgraph.prebuilt.create_react_agent: %s", e) @@ -279,19 +281,19 @@ def _wrap_agent_factories(self, tracer): # Patch the actual module where the function is defined try: wrap_function_wrapper( - "langchain.agents.factory", - "create_agent", - agent_wrapper, - ) + "langchain.agents.factory", + "create_agent", + agent_wrapper, + ) except Exception as e: logger.debug("Failed to wrap langchain.agents.factory.create_agent: %s", e) # Also patch the re-export location for imports from langchain.agents try: wrap_function_wrapper( - "langchain.agents", - "create_agent", - agent_wrapper, - ) + "langchain.agents", + "create_agent", + agent_wrapper, + ) except Exception as e: logger.debug("Failed to wrap langchain.agents.create_agent: %s", e) @@ -437,17 +439,12 @@ def __call__( TraceContextTextMapPropagator().inject(extra_headers, context=ctx) kwargs["extra_headers"] = extra_headers else: - logger.debug( - "No span found for run_id %s, skipping header injection", - run_id - ) + logger.debug("No span found for run_id %s, skipping header injection", run_id) # In legacy chains like LLMChain, suppressing model instrumentations # within create_llm_span doesn't work, so this should helps as a fallback try: - context_api.attach( - context_api.set_value(SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY, True) - ) + context_api.attach(context_api.set_value(SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY, True)) except Exception: # If context setting fails, continue without suppression # This is not critical for core functionality diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py index 15ed817e15..c5f26a7063 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py @@ -82,8 +82,7 @@ # Context variable for tracking current LangGraph node (for Command source tracking) # Using ContextVar instead of OTel context to avoid detach issues in async scenarios _langgraph_current_node: contextvars.ContextVar[str | None] = contextvars.ContextVar( - 'langgraph_current_node', - default=None + "langgraph_current_node", default=None ) @@ -165,9 +164,7 @@ def _extract_tool_call_data( class TraceloopCallbackHandler(BaseCallbackHandler): - def __init__( - self, tracer: Tracer, duration_histogram: Histogram, token_histogram: Histogram - ) -> None: + def __init__(self, tracer: Tracer, duration_histogram: Histogram, token_histogram: Histogram) -> None: """Initialize the callback handler state used to track active LangChain spans.""" super().__init__() self.tracer = tracer @@ -293,15 +290,9 @@ def _create_span( association_properties_token = None if metadata is not None: - current_association_properties = ( - context_api.get_value("association_properties") or {} - ) + current_association_properties = context_api.get_value("association_properties") or {} # Sanitize metadata values to ensure they're compatible with OpenTelemetry - sanitized_metadata = { - k: _sanitize_metadata_value(v) - for k, v in metadata.items() - if v is not None - } + sanitized_metadata = {k: _sanitize_metadata_value(v) for k, v in metadata.items() if v is not None} try: association_properties_token = context_api.attach( context_api.set_value( @@ -454,9 +445,7 @@ def _create_task_span( _set_span_attribute(span, SpanAttributes.GEN_AI_TASK_NAME, name) _set_span_attribute(span, SpanAttributes.GEN_AI_TASK_ID, str(run_id)) if parent_run_id: - _set_span_attribute( - span, SpanAttributes.GEN_AI_TASK_PARENT_ID, str(parent_run_id) - ) + _set_span_attribute(span, SpanAttributes.GEN_AI_TASK_PARENT_ID, str(parent_run_id)) return span @@ -488,9 +477,7 @@ def _create_llm_span( metadata=metadata, ) - vendor = detect_vendor_from_class( - _extract_class_name_from_serialized(serialized) - ) + vendor = detect_vendor_from_class(_extract_class_name_from_serialized(serialized)) _set_span_attribute(span, GenAIAttributes.GEN_AI_PROVIDER_NAME, vendor) operation_name = ( @@ -498,9 +485,7 @@ def _create_llm_span( if request_type == LLMRequestTypeValues.CHAT else GenAiOperationNameValues.TEXT_COMPLETION.value ) - _set_span_attribute( - span, GenAIAttributes.GEN_AI_OPERATION_NAME, operation_name - ) + _set_span_attribute(span, GenAIAttributes.GEN_AI_OPERATION_NAME, operation_name) # _create_span has already attached the span context and stored a new # SpanHolder(span, span_token, ...). Detach that span_token before @@ -514,9 +499,7 @@ def _create_llm_span( # we already have an LLM span by this point, # so skip any downstream instrumentation from here try: - token = context_api.attach( - context_api.set_value(SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY, True) - ) + token = context_api.attach(context_api.set_value(SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY, True)) except Exception: # If context setting fails, continue without suppression token token = None @@ -529,9 +512,7 @@ def _create_llm_span( workflow_name, None, entity_path, - association_properties_token=( - current_holder.association_properties_token if current_holder else None - ), + association_properties_token=(current_holder.association_properties_token if current_holder else None), ) return span @@ -599,9 +580,7 @@ def on_chain_start( configurable = config.get("configurable", {}) if isinstance(config, dict) else {} thread_id = configurable.get("thread_id") if thread_id: - _set_span_attribute( - span, GenAIAttributes.GEN_AI_CONVERSATION_ID, str(thread_id) - ) + _set_span_attribute(span, GenAIAttributes.GEN_AI_CONVERSATION_ID, str(thread_id)) # Set current node in context for Command source tracking. # Using ContextVar instead of OTel context to avoid detach issues in async scenarios. @@ -737,33 +716,21 @@ def on_llm_end( model_name = None if response.llm_output is not None: - model_name = response.llm_output.get( - "model_name" - ) or response.llm_output.get("model_id") + model_name = response.llm_output.get("model_name") or response.llm_output.get("model_id") if model_name is not None: - _set_span_attribute( - span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, model_name or "unknown" - ) + _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, model_name or "unknown") if self.spans[run_id].request_model is None: - _set_span_attribute( - span, GenAIAttributes.GEN_AI_REQUEST_MODEL, model_name - ) + _set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_MODEL, model_name) id = response.llm_output.get("id") if id is not None and id != "": _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_ID, id) if model_name is None: model_name = extract_model_name_from_response_metadata(response) if model_name is None and hasattr(context_api, "get_value"): - association_properties = ( - context_api.get_value("association_properties") or {} - ) - model_name = _extract_model_name_from_association_metadata( - association_properties - ) - token_usage = (response.llm_output or {}).get("token_usage") or ( - response.llm_output or {} - ).get("usage") + association_properties = context_api.get_value("association_properties") or {} + model_name = _extract_model_name_from_association_metadata(association_properties) + token_usage = (response.llm_output or {}).get("token_usage") or (response.llm_output or {}).get("usage") if token_usage is not None: prompt_tokens = ( token_usage.get("prompt_tokens") @@ -775,19 +742,11 @@ def on_llm_end( or token_usage.get("generated_token_count") or token_usage.get("output_tokens") ) - total_tokens = token_usage.get("total_tokens") or ( - prompt_tokens + completion_tokens - ) + total_tokens = token_usage.get("total_tokens") or (prompt_tokens + completion_tokens) - _set_span_attribute( - span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, prompt_tokens - ) - _set_span_attribute( - span, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, completion_tokens - ) - _set_span_attribute( - span, SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens - ) + _set_span_attribute(span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, prompt_tokens) + _set_span_attribute(span, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, completion_tokens) + _set_span_attribute(span, SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) # Record token usage metrics vendor = span.attributes.get(GenAIAttributes.GEN_AI_PROVIDER_NAME, "langchain") @@ -810,9 +769,7 @@ def on_llm_end( GenAIAttributes.GEN_AI_RESPONSE_MODEL: model_name or "unknown", }, ) - set_chat_response_usage( - span, response, self.token_histogram, token_usage is None, model_name - ) + set_chat_response_usage(span, response, self.token_histogram, token_usage is None, model_name) if should_emit_events(): self._emit_llm_end_events(response) # Also set span attributes for backward compatibility @@ -992,11 +949,8 @@ def on_retriever_end( # Extract document content for output docs_output = [] for doc in documents: - if hasattr(doc, 'page_content'): - docs_output.append({ - "page_content": doc.page_content, - "metadata": getattr(doc, 'metadata', {}) - }) + if hasattr(doc, "page_content"): + docs_output.append({"page_content": doc.page_content, "metadata": getattr(doc, "metadata", {})}) else: docs_output.append(str(doc)) @@ -1031,10 +985,7 @@ def get_entity_path(self, parent_run_id: str): if parent_span is None: return "" - elif ( - parent_span.entity_path == "" - and parent_span.entity_name == parent_span.workflow_name - ): + elif parent_span.entity_path == "" and parent_span.entity_name == parent_span.workflow_name: return "" elif parent_span.entity_path == "": return f"{parent_span.entity_name}" @@ -1145,9 +1096,7 @@ def _emit_llm_end_events(self, response): def _emit_generation_choice_event( self, index: int, - generation: Union[ - ChatGeneration, ChatGenerationChunk, Generation, GenerationChunk - ], + generation: Union[ChatGeneration, ChatGenerationChunk, Generation, GenerationChunk], ): """Emit the appropriate GenAI choice event for a single generation.""" if isinstance(generation, (ChatGeneration, ChatGenerationChunk)): @@ -1158,17 +1107,12 @@ def _emit_generation_choice_event( finish_reason = _map_finish_reason(raw_finish_reason) if raw_finish_reason else None # Get tool calls - if ( - hasattr(generation.message, "tool_calls") - and generation.message.tool_calls - ): + if hasattr(generation.message, "tool_calls") and generation.message.tool_calls: tool_calls = _extract_tool_call_data(generation.message.tool_calls) - elif hasattr( - generation.message, "additional_kwargs" - ) and generation.message.additional_kwargs.get("function_call"): - tool_calls = _extract_tool_call_data( - [generation.message.additional_kwargs.get("function_call")] - ) + elif hasattr(generation.message, "additional_kwargs") and generation.message.additional_kwargs.get( + "function_call" + ): + tool_calls = _extract_tool_call_data([generation.message.additional_kwargs.get("function_call")]) else: tool_calls = None diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/event_emitter.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/event_emitter.py index caf7a5f4cd..a38e34304c 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/event_emitter.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/event_emitter.py @@ -74,11 +74,7 @@ def _emit_message_event(event: MessageEvent) -> None: for tool_call in body["tool_calls"]: tool_call["function"].pop("arguments", None) - log_record = LogRecord( - body=body, - attributes=EVENT_ATTRIBUTES, - event_name=name - ) + log_record = LogRecord(body=body, attributes=EVENT_ATTRIBUTES, event_name=name) Config.event_logger.emit(log_record) @@ -98,9 +94,5 @@ def _emit_choice_event(event: ChoiceEvent) -> None: for tool_call in body["tool_calls"]: tool_call["function"].pop("arguments", None) - log_record = LogRecord( - body=body, - attributes=EVENT_ATTRIBUTES, - event_name="gen_ai.choice" - ) + log_record = LogRecord(body=body, attributes=EVENT_ATTRIBUTES, event_name="gen_ai.choice") Config.event_logger.emit(log_record) diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/patch.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/patch.py index 1adfed4f39..7bbde5120d 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/patch.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/patch.py @@ -13,13 +13,16 @@ logger = logging.getLogger(__name__) + # Import ContextVar for reading current node (set by callback_handler) # Importing at runtime to avoid circular import issues def _get_current_node_contextvar(): """Lazy import to avoid circular dependency.""" from opentelemetry.instrumentation.langchain.callback_handler import _langgraph_current_node + return _langgraph_current_node + # Context key for marking LangGraph flow LANGGRAPH_FLOW_KEY = "langgraph_flow" # Context key for storing the graph SpanHolder as parent for callback-created spans @@ -28,13 +31,7 @@ def _get_current_node_contextvar(): LANGGRAPH_FIRST_CHILD_PENDING_KEY = "langgraph_first_child_pending" -def _set_graph_span_attributes( - graph_span: Span, - instance: Any, - graph_name: str, - kwargs: dict, - args: tuple -) -> None: +def _set_graph_span_attributes(graph_span: Span, instance: Any, graph_name: str, kwargs: dict, args: tuple) -> None: """ Set common GenAI attributes on graph span. @@ -52,13 +49,11 @@ def _set_graph_span_attributes( # Set GenAI semantic convention attributes graph_span.set_attribute(GenAIAttributes.GEN_AI_PROVIDER_NAME, "langgraph") - graph_span.set_attribute( - GenAIAttributes.GEN_AI_OPERATION_NAME, GenAiOperationNameValues.INVOKE_AGENT.value - ) + graph_span.set_attribute(GenAIAttributes.GEN_AI_OPERATION_NAME, GenAiOperationNameValues.INVOKE_AGENT.value) graph_span.set_attribute(GenAIAttributes.GEN_AI_AGENT_NAME, graph_name) # Extract conversation ID from config - config = kwargs.get('config') or (args[1] if len(args) > 1 else None) + config = kwargs.get("config") or (args[1] if len(args) > 1 else None) if config and isinstance(config, dict): configurable = config.get("configurable", {}) thread_id = configurable.get("thread_id") @@ -91,16 +86,16 @@ def _get_graph_name(instance, args, kwargs) -> str: if len(args) > 1: config = args[1] if config is None: - config = kwargs.get('config') + config = kwargs.get("config") # Try run_name from config first (config could be RunnableConfig object, not dict) if config and isinstance(config, dict): - run_name = config.get('run_name') + run_name = config.get("run_name") if run_name: return run_name # Fallback to instance.get_name() to match LangGraph behavior - if hasattr(instance, 'get_name'): + if hasattr(instance, "get_name"): return instance.get_name() # Default @@ -118,14 +113,13 @@ def create_graph_invocation_wrapper(tracer: Tracer, is_async: bool = False): Returns: Wrapper function for sync or async graph invocation """ + def wrapper(wrapped, instance, args, kwargs): """Wrapper for Pregel.stream - yields from the generator while managing span lifecycle.""" graph_name = _get_graph_name(instance, args, kwargs) # Set LangGraph flow context before creating spans - langgraph_ctx = context_api.attach( - context_api.set_value(LANGGRAPH_FLOW_KEY, graph_name) - ) + langgraph_ctx = context_api.attach(context_api.set_value(LANGGRAPH_FLOW_KEY, graph_name)) # Create graph span with GenAI convention naming: invoke_agent {agent_name} graph_span = tracer.start_span(f"invoke_agent {graph_name}") @@ -165,9 +159,7 @@ async def async_wrapper(wrapped, instance, args, kwargs): graph_name = _get_graph_name(instance, args, kwargs) # Set LangGraph flow context before creating spans - langgraph_ctx = context_api.attach( - context_api.set_value(LANGGRAPH_FLOW_KEY, graph_name) - ) + langgraph_ctx = context_api.attach(context_api.set_value(LANGGRAPH_FLOW_KEY, graph_name)) # Create graph span with GenAI convention naming: invoke_agent {agent_name} graph_span = tracer.start_span(f"invoke_agent {graph_name}") @@ -219,6 +211,7 @@ def create_command_init_wrapper(tracer: Tracer): Returns: Wrapper function for Command.__init__ """ + def wrapper(wrapped, instance, args, kwargs): # Call original __init__ first result = wrapped(*args, **kwargs) @@ -240,25 +233,16 @@ def wrapper(wrapped, instance, args, kwargs): target_str = ", ".join(goto_destinations) span_name = f"goto {target_str}" - with tracer.start_as_current_span( - span_name, - kind=SpanKind.INTERNAL - ) as span: + with tracer.start_as_current_span(span_name, kind=SpanKind.INTERNAL) as span: # Set GenAI operation name span.set_attribute(GenAIAttributes.GEN_AI_OPERATION_NAME, "goto") - span.set_attribute( - SpanAttributes.LANGGRAPH_COMMAND_SOURCE_NODE, source_node - ) + span.set_attribute(SpanAttributes.LANGGRAPH_COMMAND_SOURCE_NODE, source_node) if len(goto_destinations) == 1: - span.set_attribute( - SpanAttributes.LANGGRAPH_COMMAND_GOTO_NODE, goto_destinations[0] - ) + span.set_attribute(SpanAttributes.LANGGRAPH_COMMAND_GOTO_NODE, goto_destinations[0]) else: - span.set_attribute( - SpanAttributes.LANGGRAPH_COMMAND_GOTO_NODES, json.dumps(goto_destinations) - ) + span.set_attribute(SpanAttributes.LANGGRAPH_COMMAND_GOTO_NODES, json.dumps(goto_destinations)) return result @@ -297,11 +281,7 @@ def _extract_goto_destinations(goto: Any) -> list[str]: return destinations -def _set_middleware_span_attributes( - span: Span, - middleware_name: str, - hook_name: str -) -> None: +def _set_middleware_span_attributes(span: Span, middleware_name: str, hook_name: str) -> None: """ Set common GenAI attributes on middleware span. @@ -318,9 +298,7 @@ def _set_middleware_span_attributes( GenAICustomOperationName.EXECUTE_TASK.value, ) span.set_attribute(SpanAttributes.GEN_AI_TASK_KIND, middleware_name) - span.set_attribute( - SpanAttributes.GEN_AI_TASK_NAME, f"{middleware_name}.{hook_name}" - ) + span.set_attribute(SpanAttributes.GEN_AI_TASK_NAME, f"{middleware_name}.{hook_name}") span.set_attribute(GenAIAttributes.GEN_AI_PROVIDER_NAME, "langchain") @@ -339,6 +317,7 @@ def create_middleware_hook_wrapper(tracer: Tracer, hook_name: str): Returns: Wrapper function for the middleware hook """ + def wrapper(wrapped, instance, args, kwargs): middleware_name = instance.__class__.__name__ span_name = f"execute_task {middleware_name}.{hook_name}" @@ -369,6 +348,7 @@ def create_async_middleware_hook_wrapper(tracer: Tracer, hook_name: str): Returns: Async wrapper function for the middleware hook """ + async def async_wrapper(wrapped, instance, args, kwargs): middleware_name = instance.__class__.__name__ span_name = f"execute_task {middleware_name}.{hook_name}" @@ -397,36 +377,36 @@ def _extract_tool_definition(tool: Any) -> dict | None: tool_def = {"type": "function"} # Extract name - if hasattr(tool, 'name'): + if hasattr(tool, "name"): tool_def["name"] = tool.name - elif isinstance(tool, dict) and 'name' in tool: - tool_def["name"] = tool['name'] - elif hasattr(tool, '__name__'): + elif isinstance(tool, dict) and "name" in tool: + tool_def["name"] = tool["name"] + elif hasattr(tool, "__name__"): tool_def["name"] = tool.__name__ else: return None # Extract description - if hasattr(tool, 'description'): + if hasattr(tool, "description"): tool_def["description"] = tool.description - elif isinstance(tool, dict) and 'description' in tool: - tool_def["description"] = tool['description'] - elif hasattr(tool, '__doc__') and tool.__doc__: + elif isinstance(tool, dict) and "description" in tool: + tool_def["description"] = tool["description"] + elif hasattr(tool, "__doc__") and tool.__doc__: tool_def["description"] = tool.__doc__ # Extract parameters schema parameters = None - if hasattr(tool, 'args_schema') and tool.args_schema: + if hasattr(tool, "args_schema") and tool.args_schema: # LangChain tools with Pydantic schema try: - if hasattr(tool.args_schema, 'model_json_schema'): + if hasattr(tool.args_schema, "model_json_schema"): parameters = tool.args_schema.model_json_schema() - elif hasattr(tool.args_schema, 'schema'): + elif hasattr(tool.args_schema, "schema"): parameters = tool.args_schema.schema() except Exception: pass - elif isinstance(tool, dict) and 'parameters' in tool: - parameters = tool['parameters'] + elif isinstance(tool, dict) and "parameters" in tool: + parameters = tool["parameters"] if parameters: tool_def["parameters"] = parameters @@ -447,14 +427,15 @@ def create_agent_wrapper(tracer: Tracer, provider_name: str = "langchain"): Returns: Wrapper function for agent factory """ + def wrapper(wrapped, _instance, args, kwargs): # Extract agent name from kwargs or use function name agent_name = kwargs.get("name") if not agent_name: # Use the wrapped function's name as fallback - agent_name = getattr(wrapped, '__name__', 'agent') + agent_name = getattr(wrapped, "__name__", "agent") # Clean up the name (e.g., "create_react_agent" -> "react_agent") - if agent_name.startswith('create_'): + if agent_name.startswith("create_"): agent_name = agent_name[7:] span_name = f"create_agent {agent_name}" @@ -473,11 +454,9 @@ def wrapper(wrapped, _instance, args, kwargs): if system_instructions: if isinstance(system_instructions, str): span.set_attribute(GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS, system_instructions) - elif hasattr(system_instructions, 'content'): + elif hasattr(system_instructions, "content"): # SystemMessage or similar object with content attribute - span.set_attribute( - GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS, str(system_instructions.content) - ) + span.set_attribute(GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS, str(system_instructions.content)) # Extract tool definitions in OpenAI function format # Tools can be in args[1] (positional) or kwargs @@ -488,20 +467,13 @@ def wrapper(wrapped, _instance, args, kwargs): tool_definitions = [] # ToolNode wraps tools but is not itself iterable; # fall back to its tools_by_name dict when available. - tools_iter = ( - tools.tools_by_name.values() - if hasattr(tools, "tools_by_name") - else tools - ) + tools_iter = tools.tools_by_name.values() if hasattr(tools, "tools_by_name") else tools for tool in tools_iter: tool_def = _extract_tool_definition(tool) if tool_def: tool_definitions.append(tool_def) if tool_definitions: - span.set_attribute( - GenAIAttributes.GEN_AI_TOOL_DEFINITIONS, - json.dumps(tool_definitions) - ) + span.set_attribute(GenAIAttributes.GEN_AI_TOOL_DEFINITIONS, json.dumps(tool_definitions)) result = wrapped(*args, **kwargs) diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py index 09d365daaf..34fe221572 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/span_utils.py @@ -108,12 +108,14 @@ def _content_to_parts(content) -> list[dict]: parts.append({"type": "uri", "modality": "image", "uri": url}) elif block_type == "image": # base64 image block - parts.append({ - "type": "blob", - "modality": "image", - "mime_type": block.get("media_type", block.get("mime_type", "")), - "content": block.get("data", ""), - }) + parts.append( + { + "type": "blob", + "modality": "image", + "mime_type": block.get("media_type", block.get("mime_type", "")), + "content": block.get("data", ""), + } + ) else: # Unknown block type — preserve as text with JSON parts.append({"type": "text", "content": json.dumps(block, cls=CallbackFilteredJSONEncoder)}) @@ -131,12 +133,8 @@ def _tool_calls_to_parts(tool_calls) -> list[dict]: for tc in tool_calls: tc_dict = dict(tc) tool_id = tc_dict.get("id", "") - tool_name = tc_dict.get( - "name", tc_dict.get("function", {}).get("name", "") - ) - tool_args = tc_dict.get( - "args", tc_dict.get("function", {}).get("arguments") - ) + tool_name = tc_dict.get("name", tc_dict.get("function", {}).get("name", "")) + tool_args = tc_dict.get("args", tc_dict.get("function", {}).get("arguments")) if isinstance(tool_args, str): try: tool_args = json.loads(tool_args) @@ -161,9 +159,7 @@ def set_request_params(span, kwargs, span_holder: SpanHolder): if (model := kwargs.get(model_tag)) is not None: span_holder.request_model = model break - elif ( - model := (kwargs.get("invocation_params") or {}).get(model_tag) - ) is not None: + elif (model := (kwargs.get("invocation_params") or {}).get(model_tag)) is not None: span_holder.request_model = model break else: @@ -174,9 +170,7 @@ def set_request_params(span, kwargs, span_holder: SpanHolder): _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, model) if "invocation_params" in kwargs: - params = ( - kwargs["invocation_params"].get("params") or kwargs["invocation_params"] - ) + params = kwargs["invocation_params"].get("params") or kwargs["invocation_params"] else: params = kwargs @@ -185,9 +179,7 @@ def set_request_params(span, kwargs, span_holder: SpanHolder): GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS, params.get("max_tokens") or params.get("max_new_tokens"), ) - _set_span_attribute( - span, GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE, params.get("temperature") - ) + _set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE, params.get("temperature")) _set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_TOP_P, params.get("top_p")) tools = kwargs.get("invocation_params", {}).get("tools", []) @@ -221,10 +213,12 @@ def set_llm_request( if should_send_prompts(): input_messages = [] for msg in prompts: - input_messages.append({ - "role": "user", - "parts": [{"type": "text", "content": msg}], - }) + input_messages.append( + { + "role": "user", + "parts": [{"type": "text", "content": msg}], + } + ) if input_messages: _set_span_attribute( span, @@ -278,19 +272,19 @@ def set_chat_request( if isinstance(msg.content, str) else json.dumps(msg.content, cls=CallbackFilteredJSONEncoder) ) - parts = [{ - "type": "tool_call_response", - "id": msg.tool_call_id, - "response": content_str, - }] + parts = [ + { + "type": "tool_call_response", + "id": msg.tool_call_id, + "response": content_str, + } + ] else: parts = _content_to_parts(msg.content) # Tool calls (for assistant messages) tool_calls = ( - msg.tool_calls - if hasattr(msg, "tool_calls") - else msg.additional_kwargs.get("tool_calls") + msg.tool_calls if hasattr(msg, "tool_calls") else msg.additional_kwargs.get("tool_calls") ) if tool_calls: parts.extend(_tool_calls_to_parts(tool_calls)) @@ -354,12 +348,14 @@ def set_chat_response(span: Span, response: LLMResult) -> None: fc_args = json.loads(fc_args) except (json.JSONDecodeError, TypeError): pass - parts.append({ - "type": "tool_call", - "id": "", - "name": fc.get("name"), - "arguments": fc_args, - }) + parts.append( + { + "type": "tool_call", + "id": "", + "name": fc.get("name"), + "arguments": fc_args, + } + ) # Handle new tool_calls format (multiple tool calls) tool_calls = ( @@ -384,11 +380,7 @@ def set_chat_response(span: Span, response: LLMResult) -> None: def set_chat_response_usage( - span: Span, - response: LLMResult, - token_histogram: Histogram, - record_token_usage: bool, - model_name: str + span: Span, response: LLMResult, token_histogram: Histogram, record_token_usage: bool, model_name: str ) -> None: input_tokens = 0 output_tokens = 0 @@ -421,9 +413,7 @@ def set_chat_response_usage( total_tokens = input_tokens + output_tokens if generation.message.usage_metadata.get("input_token_details"): - input_token_details = generation.message.usage_metadata.get( - "input_token_details", {} - ) + input_token_details = generation.message.usage_metadata.get("input_token_details", {}) raw_cache_read = input_token_details.get("cache_read") if isinstance(raw_cache_read, (int, float)): cache_read_tokens = (cache_read_tokens or 0) + raw_cache_read diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/utils.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/utils.py index 77449279b7..0c568a4dce 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/utils.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/utils.py @@ -47,9 +47,9 @@ def default(self, o): def should_send_prompts(): - return ( - os.getenv(TRACELOOP_TRACE_CONTENT) or "true" - ).lower() == "true" or context_api.get_value("override_enable_content_tracing") + return (os.getenv(TRACELOOP_TRACE_CONTENT) or "true").lower() == "true" or context_api.get_value( + "override_enable_content_tracing" + ) def dont_throw(func): @@ -82,9 +82,7 @@ def should_emit_events() -> bool: Checks if the instrumentation isn't using the legacy attributes and if the event logger is not None. """ - return not Config.use_legacy_attributes and isinstance( - Config.event_logger, Logger - ) + return not Config.use_legacy_attributes and isinstance(Config.event_logger, Logger) def is_package_available(package_name): diff --git a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/vendor_detection.py b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/vendor_detection.py index 7ce4a34e6a..e691214e13 100644 --- a/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/vendor_detection.py +++ b/packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/vendor_detection.py @@ -26,79 +26,48 @@ def _get_vendor_rules() -> List[VendorRule]: VendorRule( exact_matches={"AzureChatOpenAI", "AzureOpenAI", "AzureOpenAIEmbeddings"}, patterns=["azure"], - vendor_name="azure.ai.openai" + vendor_name="azure.ai.openai", ), VendorRule( - exact_matches={"ChatOpenAI", "OpenAI", "OpenAIEmbeddings"}, - patterns=["openai"], - vendor_name="openai" + exact_matches={"ChatOpenAI", "OpenAI", "OpenAIEmbeddings"}, patterns=["openai"], vendor_name="openai" ), VendorRule( exact_matches={"ChatBedrock", "BedrockEmbeddings", "Bedrock", "BedrockChat"}, patterns=["bedrock", "aws"], - vendor_name="aws.bedrock" - ), - VendorRule( - exact_matches={"ChatAnthropic", "AnthropicLLM"}, - patterns=["anthropic"], - vendor_name="anthropic" + vendor_name="aws.bedrock", ), + VendorRule(exact_matches={"ChatAnthropic", "AnthropicLLM"}, patterns=["anthropic"], vendor_name="anthropic"), VendorRule( exact_matches={"ChatVertexAI", "VertexAI", "VertexAIEmbeddings"}, patterns=["vertex"], - vendor_name="gcp.vertex_ai" + vendor_name="gcp.vertex_ai", ), VendorRule( - exact_matches={ - "ChatGoogleGenerativeAI", "GoogleGenerativeAI", - "GooglePaLM", "ChatGooglePaLM" - }, + exact_matches={"ChatGoogleGenerativeAI", "GoogleGenerativeAI", "GooglePaLM", "ChatGooglePaLM"}, patterns=["google", "palm", "gemini"], - vendor_name="gcp.gen_ai" + vendor_name="gcp.gen_ai", ), VendorRule( - exact_matches={"ChatCohere", "CohereEmbeddings", "Cohere"}, - patterns=["cohere"], - vendor_name="cohere" + exact_matches={"ChatCohere", "CohereEmbeddings", "Cohere"}, patterns=["cohere"], vendor_name="cohere" ), VendorRule( exact_matches={ - "HuggingFacePipeline", "HuggingFaceTextGenInference", - "HuggingFaceEmbeddings", "ChatHuggingFace" + "HuggingFacePipeline", + "HuggingFaceTextGenInference", + "HuggingFaceEmbeddings", + "ChatHuggingFace", }, patterns=["huggingface"], - vendor_name="hugging_face" - ), - VendorRule( - exact_matches={"ChatOllama", "OllamaEmbeddings", "Ollama"}, - patterns=["ollama"], - vendor_name="ollama" - ), - VendorRule( - exact_matches={"Together", "ChatTogether"}, - patterns=["together"], - vendor_name="together_ai" - ), - VendorRule( - exact_matches={"Replicate", "ChatReplicate"}, - patterns=["replicate"], - vendor_name="replicate" - ), - VendorRule( - exact_matches={"ChatFireworks", "Fireworks"}, - patterns=["fireworks"], - vendor_name="fireworks" - ), - VendorRule( - exact_matches={"ChatGroq"}, - patterns=["groq"], - vendor_name="groq" + vendor_name="hugging_face", ), VendorRule( - exact_matches={"ChatMistralAI", "MistralAI"}, - patterns=["mistral"], - vendor_name="mistral_ai" + exact_matches={"ChatOllama", "OllamaEmbeddings", "Ollama"}, patterns=["ollama"], vendor_name="ollama" ), + VendorRule(exact_matches={"Together", "ChatTogether"}, patterns=["together"], vendor_name="together_ai"), + VendorRule(exact_matches={"Replicate", "ChatReplicate"}, patterns=["replicate"], vendor_name="replicate"), + VendorRule(exact_matches={"ChatFireworks", "Fireworks"}, patterns=["fireworks"], vendor_name="fireworks"), + VendorRule(exact_matches={"ChatGroq"}, patterns=["groq"], vendor_name="groq"), + VendorRule(exact_matches={"ChatMistralAI", "MistralAI"}, patterns=["mistral"], vendor_name="mistral_ai"), ] diff --git a/packages/opentelemetry-instrumentation-langchain/tests/conftest.py b/packages/opentelemetry-instrumentation-langchain/tests/conftest.py index b41cf016db..1cf6201cdc 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/conftest.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/conftest.py @@ -55,9 +55,7 @@ def fixture_logger_provider(log_exporter): @pytest.fixture(scope="session", name="reader") def fixture_reader(): - reader = InMemoryMetricReader( - {Counter: AggregationTemporality.DELTA, Histogram: AggregationTemporality.DELTA} - ) + reader = InMemoryMetricReader({Counter: AggregationTemporality.DELTA, Histogram: AggregationTemporality.DELTA}) return reader @@ -72,14 +70,10 @@ def fixture_meter_provider(reader): @pytest.fixture(scope="session") def instrument_legacy(reader, tracer_provider, meter_provider): openai_instrumentor = OpenAIInstrumentor() - openai_instrumentor.instrument( - tracer_provider=tracer_provider, meter_provider=meter_provider - ) + openai_instrumentor.instrument(tracer_provider=tracer_provider, meter_provider=meter_provider) langchain_instrumentor = LangchainInstrumentor() - langchain_instrumentor.instrument( - tracer_provider=tracer_provider, meter_provider=meter_provider - ) + langchain_instrumentor.instrument(tracer_provider=tracer_provider, meter_provider=meter_provider) bedrock_instrumentor = BedrockInstrumentor() bedrock_instrumentor.instrument(tracer_provider=tracer_provider) @@ -96,9 +90,7 @@ def instrument_with_content(instrument_legacy, logger_provider): os.environ.update({TRACELOOP_TRACE_CONTENT: "True"}) Config.use_legacy_attributes = False - Config.event_logger = logger_provider.get_logger( - __name__, __version__ - ) + Config.event_logger = logger_provider.get_logger(__name__, __version__) instrumentor = instrument_legacy yield instrumentor @@ -113,9 +105,7 @@ def instrument_with_no_content(instrument_legacy, logger_provider): os.environ.update({TRACELOOP_TRACE_CONTENT: "False"}) Config.use_legacy_attributes = False - Config.event_logger = logger_provider.get_logger( - __name__, __version__ - ) + Config.event_logger = logger_provider.get_logger(__name__, __version__) instrumentor = instrument_legacy yield instrumentor @@ -150,11 +140,7 @@ def before_record_request(request): try: if isinstance(request.body, (str, bytes)): - body_str = ( - request.body.decode("utf-8") - if isinstance(request.body, bytes) - else request.body - ) + body_str = request.body.decode("utf-8") if isinstance(request.body, bytes) else request.body body_data = json.loads(body_str) if "api_key" in body_data: body_data["api_key"] = "FILTERED" diff --git a/packages/opentelemetry-instrumentation-langchain/tests/metrics/test_langchain_metrics.py b/packages/opentelemetry-instrumentation-langchain/tests/metrics/test_langchain_metrics.py index a812026afc..396a555cd1 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/metrics/test_langchain_metrics.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/metrics/test_langchain_metrics.py @@ -53,24 +53,14 @@ def test_llm_chain_metrics(instrument_legacy, reader, chain): "input", ] assert data_point.sum > 0 - assert ( - data_point.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] - == "openai" - ) + assert data_point.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] == "openai" if metric.name == Meters.LLM_OPERATION_DURATION: found_duration_metric = False # Not generating duration metric - assert any( - data_point.count > 0 for data_point in metric.data.data_points - ) - assert any( - data_point.sum > 0 for data_point in metric.data.data_points - ) + assert any(data_point.count > 0 for data_point in metric.data.data_points) + assert any(data_point.sum > 0 for data_point in metric.data.data_points) for data_point in metric.data.data_points: - assert ( - data_point.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] - == "openai" - ) + assert data_point.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] == "openai" assert found_token_metric is False # Metrics not generated assert found_duration_metric is False # Metrics not generated @@ -105,24 +95,14 @@ def test_llm_chain_streaming_metrics(instrument_legacy, reader, llm): "input", ] assert data_point.sum > 0 - assert ( - data_point.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] - == "openai" - ) + assert data_point.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] == "openai" if metric.name == Meters.LLM_OPERATION_DURATION: found_duration_metric = False # Not generating duration metric - assert any( - data_point.count > 0 for data_point in metric.data.data_points - ) - assert any( - data_point.sum > 0 for data_point in metric.data.data_points - ) + assert any(data_point.count > 0 for data_point in metric.data.data_points) + assert any(data_point.sum > 0 for data_point in metric.data.data_points) for data_point in metric.data.data_points: - assert ( - data_point.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] - == "openai" - ) + assert data_point.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] == "openai" assert found_token_metric is False # Metrics not generated assert found_duration_metric is False # Metrics not generated @@ -184,7 +164,7 @@ def patched_generate(*args, **kwargs): result.llm_output = None return result - with patch.object(llm, '_generate', side_effect=patched_generate): + with patch.object(llm, "_generate", side_effect=patched_generate): chain.run(product="colorful socks") found_token_metric, found_duration_metric = verify_langchain_metrics(reader) @@ -203,12 +183,10 @@ def calculate(state: State): request = state["request"] completion = openai_client.chat.completions.create( model="gpt-4o", - messages=[ - {"role": "system", "content": "You are a mathematician."}, - {"role": "user", "content": request} - ] + messages=[{"role": "system", "content": "You are a mathematician."}, {"role": "user", "content": request}], ) return {"result": completion.choices[0].message.content} + workflow = StateGraph(State) workflow.add_node("calculate", calculate) workflow.set_entry_point("calculate") @@ -226,11 +204,7 @@ def calculate(state: State): assert len(metric_data) == 3 token_usage_metric = next( - ( - m - for m in metric_data - if m.name == Meters.LLM_TOKEN_USAGE - ), + (m for m in metric_data if m.name == Meters.LLM_TOKEN_USAGE), None, ) assert token_usage_metric is not None @@ -238,17 +212,12 @@ def calculate(state: State): assert token_usage_data_point.sum > 0 # These metrics come from the OpenAI instrumentation (direct client usage), # which now uses GEN_AI_PROVIDER_NAME instead of GEN_AI_SYSTEM - assert ( - token_usage_data_point.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] == "openai" - and token_usage_data_point.attributes[GenAIAttributes.GEN_AI_TOKEN_TYPE] in ["input", "output"] - ) + assert token_usage_data_point.attributes[ + GenAIAttributes.GEN_AI_PROVIDER_NAME + ] == "openai" and token_usage_data_point.attributes[GenAIAttributes.GEN_AI_TOKEN_TYPE] in ["input", "output"] duration_metric = next( - ( - m - for m in metric_data - if m.name == Meters.LLM_OPERATION_DURATION - ), + (m for m in metric_data if m.name == Meters.LLM_OPERATION_DURATION), None, ) assert duration_metric is not None @@ -256,19 +225,9 @@ def calculate(state: State): assert duration_data_point.sum > 0 assert duration_data_point.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] == "openai" - generation_choices_metric = next( - ( - m - for m in metric_data - if m.name == Meters.LLM_GENERATION_CHOICES - ), - None - ) + generation_choices_metric = next((m for m in metric_data if m.name == Meters.LLM_GENERATION_CHOICES), None) assert generation_choices_metric is not None generation_choices_data_points = generation_choices_metric.data.data_points for data_point in generation_choices_data_points: - assert ( - data_point.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] - == "openai" - ) + assert data_point.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] == "openai" assert data_point.value > 0 diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_agents.py b/packages/opentelemetry-instrumentation-langchain/tests/test_agents.py index 75cebbc9b7..21b73ae481 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/test_agents.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_agents.py @@ -57,15 +57,11 @@ def test_agents(instrument_legacy, span_exporter, log_exporter): } logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr -def test_agents_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +def test_agents_with_events_with_content(instrument_with_content, span_exporter, log_exporter): search = TavilySearchResults(max_results=2) tools = [search] @@ -107,9 +103,7 @@ def test_agents_with_events_with_content( assert_message_in_logs(logs, "gen_ai.user.message", {"content": prompt}) # validate that the system message Event exists - assert_message_in_logs( - logs, "gen_ai.system.message", {"content": "You are a helpful assistant"} - ) + assert_message_in_logs(logs, "gen_ai.system.message", {"content": "You are a helpful assistant"}) # Validate that the assistant message Event exists assert_message_in_logs( @@ -158,9 +152,7 @@ def test_agents_with_events_with_content( @pytest.mark.vcr -def test_agents_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +def test_agents_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): search = TavilySearchResults(max_results=2) tools = [search] @@ -196,10 +188,7 @@ def test_agents_with_events_with_no_content( logs = log_exporter.get_finished_logs() assert len(logs) == 8 - assert all( - log.log_record.attributes.get(GenAIAttributes.GEN_AI_PROVIDER_NAME) == "langchain" - for log in logs - ) + assert all(log.log_record.attributes.get(GenAIAttributes.GEN_AI_PROVIDER_NAME) == "langchain" for log in logs) # Validate that the user message Event exists assert_message_in_logs(logs, "gen_ai.user.message", {}) @@ -242,11 +231,6 @@ def test_agents_with_events_with_no_content( assert_message_in_logs(logs, "gen_ai.choice", choice_event) -def assert_message_in_logs( - logs: Tuple[ReadableLogRecord, ...], event_name: str, expected_content: dict -): - assert any( - log.log_record.event_name == event_name - for log in logs - ) +def assert_message_in_logs(logs: Tuple[ReadableLogRecord, ...], event_name: str, expected_content: dict): + assert any(log.log_record.event_name == event_name for log in logs) assert any(dict(log.log_record.body) == expected_content for log in logs) diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_batch_metadata.py b/packages/opentelemetry-instrumentation-langchain/tests/test_batch_metadata.py index 65663c1cbe..2c08fe6856 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/test_batch_metadata.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_batch_metadata.py @@ -10,15 +10,8 @@ def test_batch_metadata_in_span_attributes(instrument_legacy, span_exporter): llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) # Test batch with metadata - test_metadata = { - "user_id": "12345", - "session_id": "abc-123", - "priority": "high" - } - messages_list = [ - [{"role": "user", "content": "Hello"}], - [{"role": "user", "content": "How are you?"}] - ] + test_metadata = {"user_id": "12345", "session_id": "abc-123", "priority": "high"} + messages_list = [[{"role": "user", "content": "Hello"}], [{"role": "user", "content": "How are you?"}]] # Call batch with metadata llm.batch(messages_list, config={"metadata": test_metadata}) @@ -38,9 +31,9 @@ def test_batch_metadata_in_span_attributes(instrument_legacy, span_exporter): session_id_key = f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id" priority_key = f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.priority" - assert ("user_id" in span.attributes or user_id_key in span.attributes) - assert ("session_id" in span.attributes or session_id_key in span.attributes) - assert ("priority" in span.attributes or priority_key in span.attributes) + assert "user_id" in span.attributes or user_id_key in span.attributes + assert "session_id" in span.attributes or session_id_key in span.attributes + assert "priority" in span.attributes or priority_key in span.attributes # Check the values user_id_attr = span.attributes.get("user_id") or span.attributes.get(user_id_key) @@ -60,14 +53,10 @@ async def test_async_batch_metadata_in_span_attributes(instrument_legacy, span_e llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) # Test abatch with metadata - test_metadata = { - "user_id": "67890", - "session_id": "def-456", - "environment": "production" - } + test_metadata = {"user_id": "67890", "session_id": "def-456", "environment": "production"} messages_list = [ [{"role": "user", "content": "What is AI?"}], - [{"role": "user", "content": "Explain machine learning"}] + [{"role": "user", "content": "Explain machine learning"}], ] # Call abatch with metadata @@ -88,9 +77,9 @@ async def test_async_batch_metadata_in_span_attributes(instrument_legacy, span_e session_id_key = f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id" environment_key = f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.environment" - assert ("user_id" in span.attributes or user_id_key in span.attributes) - assert ("session_id" in span.attributes or session_id_key in span.attributes) - assert ("environment" in span.attributes or environment_key in span.attributes) + assert "user_id" in span.attributes or user_id_key in span.attributes + assert "session_id" in span.attributes or session_id_key in span.attributes + assert "environment" in span.attributes or environment_key in span.attributes # Check the values user_id_attr = span.attributes.get("user_id") or span.attributes.get(user_id_key) diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_chains.py b/packages/opentelemetry-instrumentation-langchain/tests/test_chains.py index b658ff9d93..ff41149052 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/test_chains.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_chains.py @@ -21,12 +21,8 @@ def test_sequential_chain(instrument_legacy, span_exporter, log_exporter): Title: {title} Era: {era} Playwright: This is a synopsis for the above play:""" # noqa: E501 - synopsis_prompt_template = PromptTemplate( - input_variables=["title", "era"], template=synopsis_template - ) - synopsis_chain = LLMChain( - llm=llm, prompt=synopsis_prompt_template, output_key="synopsis", name="synopsis" - ) + synopsis_prompt_template = PromptTemplate(input_variables=["title", "era"], template=synopsis_template) + synopsis_chain = LLMChain(llm=llm, prompt=synopsis_prompt_template, output_key="synopsis", name="synopsis") template = """You are a play critic from the New York Times. Given the synopsis of play, it is your job to write a review for that play. @@ -43,9 +39,7 @@ def test_sequential_chain(instrument_legacy, span_exporter, log_exporter): output_variables=["synopsis", "review"], verbose=True, ) - overall_chain.invoke( - {"title": "Tragedy at sunset on the beach", "era": "Victorian England"} - ) + overall_chain.invoke({"title": "Tragedy at sunset on the beach", "era": "Victorian England"}) spans = span_exporter.get_finished_spans() @@ -57,32 +51,15 @@ def test_sequential_chain(instrument_legacy, span_exporter, log_exporter): "SequentialChain.workflow", ] == [span.name for span in spans] - workflow_span = next( - span for span in spans if span.name == "SequentialChain.workflow" - ) - task_spans = [ - span for span in spans if span.name in ["execute_task synopsis", "execute_task LLMChain"] - ] + workflow_span = next(span for span in spans if span.name == "SequentialChain.workflow") + task_spans = [span for span in spans if span.name in ["execute_task synopsis", "execute_task LLMChain"]] llm_spans = [span for span in spans if span.name == "OpenAI.completion"] assert workflow_span.attributes[SpanAttributes.TRACELOOP_SPAN_KIND] == "workflow" - assert ( - workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_NAME] - == "SequentialChain" - ) - assert all( - span.attributes[SpanAttributes.TRACELOOP_SPAN_KIND] == "task" - for span in task_spans - ) - assert all( - span.attributes[SpanAttributes.TRACELOOP_WORKFLOW_NAME] == "SequentialChain" - for span in spans - ) - assert all( - span.attributes[SpanAttributes.TRACELOOP_ENTITY_PATH] - in ["synopsis", "LLMChain"] - for span in llm_spans - ) + assert workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_NAME] == "SequentialChain" + assert all(span.attributes[SpanAttributes.TRACELOOP_SPAN_KIND] == "task" for span in task_spans) + assert all(span.attributes[SpanAttributes.TRACELOOP_WORKFLOW_NAME] == "SequentialChain" for span in spans) + assert all(span.attributes[SpanAttributes.TRACELOOP_ENTITY_PATH] in ["synopsis", "LLMChain"] for span in llm_spans) synopsis_span = next(span for span in spans if span.name == "execute_task synopsis") review_span = next(span for span in spans if span.name == "execute_task LLMChain") @@ -106,9 +83,7 @@ def test_sequential_chain(instrument_legacy, span_exporter, log_exporter): "review", } - overall_span = next( - span for span in spans if span.name == "SequentialChain.workflow" - ) + overall_span = next(span for span in spans if span.name == "SequentialChain.workflow") data = json.loads(overall_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT]) assert data["inputs"] == { "title": "Tragedy at sunset on the beach", @@ -119,39 +94,25 @@ def test_sequential_chain(instrument_legacy, span_exporter, log_exporter): assert data["outputs"].keys() == {"synopsis", "review"} openai_span = next(span for span in spans if span.name == "OpenAI.completion") - assert ( - openai_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] - == "gpt-3.5-turbo-instruct" - ) - assert ( - (openai_span.attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL]) - == "gpt-3.5-turbo-instruct" - ) + assert openai_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == "gpt-3.5-turbo-instruct" + assert (openai_span.attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL]) == "gpt-3.5-turbo-instruct" input_messages = json.loads(openai_span.attributes[GenAIAttributes.GEN_AI_INPUT_MESSAGES]) assert input_messages[0]["parts"][0]["content"] logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr -def test_sequential_chain_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +def test_sequential_chain_with_events_with_content(instrument_with_content, span_exporter, log_exporter): llm = OpenAI(temperature=0.7) synopsis_template = """You are a playwright. Given the title of play and the era it is set in, it is your job to write a synopsis for that title. Title: {title} Era: {era} Playwright: This is a synopsis for the above play:""" # noqa: E501 - synopsis_prompt_template = PromptTemplate( - input_variables=["title", "era"], template=synopsis_template - ) - synopsis_chain = LLMChain( - llm=llm, prompt=synopsis_prompt_template, output_key="synopsis", name="synopsis" - ) + synopsis_prompt_template = PromptTemplate(input_variables=["title", "era"], template=synopsis_template) + synopsis_chain = LLMChain(llm=llm, prompt=synopsis_prompt_template, output_key="synopsis", name="synopsis") template = """You are a play critic from the New York Times. Given the synopsis of play, it is your job to write a review for that play. @@ -168,9 +129,7 @@ def test_sequential_chain_with_events_with_content( output_variables=["synopsis", "review"], verbose=True, ) - response = overall_chain.invoke( - {"title": "Tragedy at sunset on the beach", "era": "Victorian England"} - ) + response = overall_chain.invoke({"title": "Tragedy at sunset on the beach", "era": "Victorian England"}) spans = span_exporter.get_finished_spans() @@ -182,42 +141,19 @@ def test_sequential_chain_with_events_with_content( "SequentialChain.workflow", ] == [span.name for span in spans] - workflow_span = next( - span for span in spans if span.name == "SequentialChain.workflow" - ) - task_spans = [ - span for span in spans if span.name in ["execute_task synopsis", "execute_task LLMChain"] - ] + workflow_span = next(span for span in spans if span.name == "SequentialChain.workflow") + task_spans = [span for span in spans if span.name in ["execute_task synopsis", "execute_task LLMChain"]] llm_spans = [span for span in spans if span.name == "OpenAI.completion"] assert workflow_span.attributes[SpanAttributes.TRACELOOP_SPAN_KIND] == "workflow" - assert ( - workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_NAME] - == "SequentialChain" - ) - assert all( - span.attributes[SpanAttributes.TRACELOOP_SPAN_KIND] == "task" - for span in task_spans - ) - assert all( - span.attributes[SpanAttributes.TRACELOOP_WORKFLOW_NAME] == "SequentialChain" - for span in spans - ) - assert all( - span.attributes[SpanAttributes.TRACELOOP_ENTITY_PATH] - in ["synopsis", "LLMChain"] - for span in llm_spans - ) + assert workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_NAME] == "SequentialChain" + assert all(span.attributes[SpanAttributes.TRACELOOP_SPAN_KIND] == "task" for span in task_spans) + assert all(span.attributes[SpanAttributes.TRACELOOP_WORKFLOW_NAME] == "SequentialChain" for span in spans) + assert all(span.attributes[SpanAttributes.TRACELOOP_ENTITY_PATH] in ["synopsis", "LLMChain"] for span in llm_spans) openai_span = next(span for span in spans if span.name == "OpenAI.completion") - assert ( - openai_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] - == "gpt-3.5-turbo-instruct" - ) - assert ( - (openai_span.attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL]) - == "gpt-3.5-turbo-instruct" - ) + assert openai_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == "gpt-3.5-turbo-instruct" + assert (openai_span.attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL]) == "gpt-3.5-turbo-instruct" logs = log_exporter.get_finished_logs() assert len(logs) == 4 @@ -226,11 +162,7 @@ def test_sequential_chain_with_events_with_content( assert_message_in_logs( logs[0], "gen_ai.user.message", - { - "content": synopsis_template.format( - title="Tragedy at sunset on the beach", era="Victorian England" - ) - }, + {"content": synopsis_template.format(title="Tragedy at sunset on the beach", era="Victorian England")}, ) # Validate AI choice Event in the first chain @@ -258,21 +190,15 @@ def test_sequential_chain_with_events_with_content( @pytest.mark.vcr -def test_sequential_chain_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +def test_sequential_chain_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): llm = OpenAI(temperature=0.7) synopsis_template = """You are a playwright. Given the title of play and the era it is set in, it is your job to write a synopsis for that title. Title: {title} Era: {era} Playwright: This is a synopsis for the above play:""" # noqa: E501 - synopsis_prompt_template = PromptTemplate( - input_variables=["title", "era"], template=synopsis_template - ) - synopsis_chain = LLMChain( - llm=llm, prompt=synopsis_prompt_template, output_key="synopsis", name="synopsis" - ) + synopsis_prompt_template = PromptTemplate(input_variables=["title", "era"], template=synopsis_template) + synopsis_chain = LLMChain(llm=llm, prompt=synopsis_prompt_template, output_key="synopsis", name="synopsis") template = """You are a play critic from the New York Times. Given the synopsis of play, it is your job to write a review for that play. @@ -289,9 +215,7 @@ def test_sequential_chain_with_events_with_no_content( output_variables=["synopsis", "review"], verbose=True, ) - overall_chain.invoke( - {"title": "Tragedy at sunset on the beach", "era": "Victorian England"} - ) + overall_chain.invoke({"title": "Tragedy at sunset on the beach", "era": "Victorian England"}) spans = span_exporter.get_finished_spans() @@ -303,42 +227,19 @@ def test_sequential_chain_with_events_with_no_content( "SequentialChain.workflow", ] == [span.name for span in spans] - workflow_span = next( - span for span in spans if span.name == "SequentialChain.workflow" - ) - task_spans = [ - span for span in spans if span.name in ["execute_task synopsis", "execute_task LLMChain"] - ] + workflow_span = next(span for span in spans if span.name == "SequentialChain.workflow") + task_spans = [span for span in spans if span.name in ["execute_task synopsis", "execute_task LLMChain"]] llm_spans = [span for span in spans if span.name == "OpenAI.completion"] assert workflow_span.attributes[SpanAttributes.TRACELOOP_SPAN_KIND] == "workflow" - assert ( - workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_NAME] - == "SequentialChain" - ) - assert all( - span.attributes[SpanAttributes.TRACELOOP_SPAN_KIND] == "task" - for span in task_spans - ) - assert all( - span.attributes[SpanAttributes.TRACELOOP_WORKFLOW_NAME] == "SequentialChain" - for span in spans - ) - assert all( - span.attributes[SpanAttributes.TRACELOOP_ENTITY_PATH] - in ["synopsis", "LLMChain"] - for span in llm_spans - ) + assert workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_NAME] == "SequentialChain" + assert all(span.attributes[SpanAttributes.TRACELOOP_SPAN_KIND] == "task" for span in task_spans) + assert all(span.attributes[SpanAttributes.TRACELOOP_WORKFLOW_NAME] == "SequentialChain" for span in spans) + assert all(span.attributes[SpanAttributes.TRACELOOP_ENTITY_PATH] in ["synopsis", "LLMChain"] for span in llm_spans) openai_span = next(span for span in spans if span.name == "OpenAI.completion") - assert ( - openai_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] - == "gpt-3.5-turbo-instruct" - ) - assert ( - (openai_span.attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL]) - == "gpt-3.5-turbo-instruct" - ) + assert openai_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == "gpt-3.5-turbo-instruct" + assert (openai_span.attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL]) == "gpt-3.5-turbo-instruct" logs = log_exporter.get_finished_logs() assert len(logs) == 4 @@ -367,12 +268,8 @@ async def test_asequential_chain(instrument_legacy, span_exporter, log_exporter) Title: {title} Era: {era} Playwright: This is a synopsis for the above play:""" # noqa: E501 - synopsis_prompt_template = PromptTemplate( - input_variables=["title", "era"], template=synopsis_template - ) - synopsis_chain = LLMChain( - llm=llm, prompt=synopsis_prompt_template, output_key="synopsis" - ) + synopsis_prompt_template = PromptTemplate(input_variables=["title", "era"], template=synopsis_template) + synopsis_chain = LLMChain(llm=llm, prompt=synopsis_prompt_template, output_key="synopsis") template = """You are a play critic from the New York Times. Given the synopsis of play, it is your job to write a review for that play. @@ -389,9 +286,7 @@ async def test_asequential_chain(instrument_legacy, span_exporter, log_exporter) output_variables=["synopsis", "review"], verbose=True, ) - await overall_chain.ainvoke( - {"title": "Tragedy at sunset on the beach", "era": "Victorian England"} - ) + await overall_chain.ainvoke({"title": "Tragedy at sunset on the beach", "era": "Victorian England"}) spans = span_exporter.get_finished_spans() @@ -403,9 +298,7 @@ async def test_asequential_chain(instrument_legacy, span_exporter, log_exporter) "SequentialChain.workflow", ] == [span.name for span in spans] - synopsis_span, review_span = [ - span for span in spans if span.name == "execute_task LLMChain" - ] + synopsis_span, review_span = [span for span in spans if span.name == "execute_task LLMChain"] data = json.loads(synopsis_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT]) assert data["inputs"] == { @@ -426,9 +319,7 @@ async def test_asequential_chain(instrument_legacy, span_exporter, log_exporter) "review", } - overall_span = next( - span for span in spans if span.name == "SequentialChain.workflow" - ) + overall_span = next(span for span in spans if span.name == "SequentialChain.workflow") data = json.loads(overall_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT]) assert data["inputs"] == { "title": "Tragedy at sunset on the beach", @@ -439,28 +330,20 @@ async def test_asequential_chain(instrument_legacy, span_exporter, log_exporter) assert data["outputs"].keys() == {"synopsis", "review"} logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr @pytest.mark.asyncio -async def test_asequential_chain_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +async def test_asequential_chain_with_events_with_content(instrument_with_content, span_exporter, log_exporter): llm = OpenAI(temperature=0.7) synopsis_template = """You are a playwright. Given the title of play and the era it is set in, it is your job to write a synopsis for that title. Title: {title} Era: {era} Playwright: This is a synopsis for the above play:""" # noqa: E501 - synopsis_prompt_template = PromptTemplate( - input_variables=["title", "era"], template=synopsis_template - ) - synopsis_chain = LLMChain( - llm=llm, prompt=synopsis_prompt_template, output_key="synopsis" - ) + synopsis_prompt_template = PromptTemplate(input_variables=["title", "era"], template=synopsis_template) + synopsis_chain = LLMChain(llm=llm, prompt=synopsis_prompt_template, output_key="synopsis") template = """You are a play critic from the New York Times. Given the synopsis of play, it is your job to write a review for that play. @@ -477,9 +360,7 @@ async def test_asequential_chain_with_events_with_content( output_variables=["synopsis", "review"], verbose=True, ) - response = await overall_chain.ainvoke( - {"title": "Tragedy at sunset on the beach", "era": "Victorian England"} - ) + response = await overall_chain.ainvoke({"title": "Tragedy at sunset on the beach", "era": "Victorian England"}) spans = span_exporter.get_finished_spans() @@ -499,9 +380,7 @@ async def test_asequential_chain_with_events_with_content( logs[0], "gen_ai.user.message", { - "content": synopsis_template.format( - title="Tragedy at sunset on the beach", era="Victorian England" - ), + "content": synopsis_template.format(title="Tragedy at sunset on the beach", era="Victorian England"), }, ) @@ -531,21 +410,15 @@ async def test_asequential_chain_with_events_with_content( @pytest.mark.vcr @pytest.mark.asyncio -async def test_asequential_chain_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +async def test_asequential_chain_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): llm = OpenAI(temperature=0.7) synopsis_template = """You are a playwright. Given the title of play and the era it is set in, it is your job to write a synopsis for that title. Title: {title} Era: {era} Playwright: This is a synopsis for the above play:""" # noqa: E501 - synopsis_prompt_template = PromptTemplate( - input_variables=["title", "era"], template=synopsis_template - ) - synopsis_chain = LLMChain( - llm=llm, prompt=synopsis_prompt_template, output_key="synopsis" - ) + synopsis_prompt_template = PromptTemplate(input_variables=["title", "era"], template=synopsis_template) + synopsis_chain = LLMChain(llm=llm, prompt=synopsis_prompt_template, output_key="synopsis") template = """You are a play critic from the New York Times. Given the synopsis of play, it is your job to write a review for that play. @@ -562,9 +435,7 @@ async def test_asequential_chain_with_events_with_no_content( output_variables=["synopsis", "review"], verbose=True, ) - await overall_chain.ainvoke( - {"title": "Tragedy at sunset on the beach", "era": "Victorian England"} - ) + await overall_chain.ainvoke({"title": "Tragedy at sunset on the beach", "era": "Victorian England"}) spans = span_exporter.get_finished_spans() @@ -576,9 +447,7 @@ async def test_asequential_chain_with_events_with_no_content( "SequentialChain.workflow", ] == [span.name for span in spans] - synopsis_span, review_span = [ - span for span in spans if span.name == "execute_task LLMChain" - ] + synopsis_span, review_span = [span for span in spans if span.name == "execute_task LLMChain"] logs = log_exporter.get_finished_logs() assert len(logs) == 4 @@ -601,9 +470,7 @@ async def test_asequential_chain_with_events_with_no_content( @pytest.mark.vcr def test_stream(instrument_legacy, span_exporter, log_exporter): chat = ChatCohere(model="command-r-08-2024", temperature=0.75) - prompt = PromptTemplate.from_template( - "write 2 lines of random text about ${product}" - ) + prompt = PromptTemplate.from_template("write 2 lines of random text about ${product}") runnable = prompt | chat | StrOutputParser() chunks = list(runnable.stream({"product": "colorful socks"})) @@ -620,15 +487,11 @@ def test_stream(instrument_legacy, span_exporter, log_exporter): assert len(chunks) == 61 logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr -def test_stream_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +def test_stream_with_events_with_content(instrument_with_content, span_exporter, log_exporter): chat = ChatCohere(model="command-r-08-2024", temperature=0.75) prompt_template = "write 2 lines of random text about ${product}" prompt = PromptTemplate.from_template(prompt_template) @@ -669,13 +532,9 @@ def test_stream_with_events_with_content( @pytest.mark.vcr -def test_stream_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +def test_stream_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): chat = ChatCohere(model="command-r-08-2024", temperature=0.75) - prompt = PromptTemplate.from_template( - "write 2 lines of random text about ${product}" - ) + prompt = PromptTemplate.from_template("write 2 lines of random text about ${product}") runnable = prompt | chat | StrOutputParser() chunks = list(runnable.stream({"product": "colorful socks"})) @@ -710,9 +569,7 @@ def test_stream_with_events_with_no_content( @pytest.mark.asyncio async def test_astream(instrument_legacy, span_exporter, log_exporter): chat = ChatCohere(model="command-r-08-2024", temperature=0.75) - prompt = PromptTemplate.from_template( - "write 2 lines of random text about ${product}" - ) + prompt = PromptTemplate.from_template("write 2 lines of random text about ${product}") runnable = prompt | chat | StrOutputParser() chunks = [] @@ -731,16 +588,12 @@ async def test_astream(instrument_legacy, span_exporter, log_exporter): assert len(chunks) == 62 logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr @pytest.mark.asyncio -async def test_astream_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +async def test_astream_with_events_with_content(instrument_with_content, span_exporter, log_exporter): chat = ChatCohere(model="command-r-08-2024", temperature=0.75) prompt_template = "write 2 lines of random text about ${product}" prompt = PromptTemplate.from_template(prompt_template) @@ -782,13 +635,9 @@ async def test_astream_with_events_with_content( @pytest.mark.vcr @pytest.mark.asyncio -async def test_astream_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +async def test_astream_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): chat = ChatCohere(model="command-r-08-2024", temperature=0.75) - prompt = PromptTemplate.from_template( - "write 2 lines of random text about ${product}" - ) + prompt = PromptTemplate.from_template("write 2 lines of random text about ${product}") runnable = prompt | chat | StrOutputParser() chunks = [] diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_context_token_lifecycle.py b/packages/opentelemetry-instrumentation-langchain/tests/test_context_token_lifecycle.py index 09c1a86e5c..33b2271e82 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/test_context_token_lifecycle.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_context_token_lifecycle.py @@ -6,6 +6,7 @@ No real HTTP is performed — the Tracer is backed by an InMemorySpanExporter. """ + from unittest.mock import MagicMock from uuid import uuid4 @@ -68,6 +69,7 @@ def _association_properties() -> dict: # Ordering fix (commit 2) — normal single-call lifecycle # --------------------------------------------------------------------------- + def test_suppression_active_after_create_llm_span(handler): """After a normal _create_llm_span call the suppression flag must be set.""" run_id = uuid4() @@ -76,8 +78,7 @@ def test_suppression_active_after_create_llm_span(handler): handler._create_llm_span(run_id, None, "gpt-4", LLMRequestTypeValues.CHAT) assert _suppression_active(), ( - "Suppression must be active so downstream OpenAI/Bedrock instrumentation " - "is skipped for this LLM call." + "Suppression must be active so downstream OpenAI/Bedrock instrumentation is skipped for this LLM call." ) span = handler.spans[run_id].span @@ -115,8 +116,7 @@ def test_association_properties_cleared_after_end_span(handler): handler._end_span(span, run_id) assert _association_properties() == {}, ( - "association_properties must be detached when the span ends; otherwise " - "later spans can inherit stale metadata." + "association_properties must be detached when the span ends; otherwise later spans can inherit stale metadata." ) @@ -124,6 +124,7 @@ def test_association_properties_cleared_after_end_span(handler): # P2 — duplicate run_id leaks supp_token_1 (issue #3957) # --------------------------------------------------------------------------- + def test_duplicate_run_id_leaks_suppression_token(handler): """ Regression test for issue #3957. @@ -233,8 +234,7 @@ def test_duplicate_run_id_replaces_association_properties(handler): handler._end_span(second_span, run_id) assert _association_properties() == {}, ( - "association_properties from the replacement span should also be cleaned up " - "when the surviving holder is ended." + "association_properties from the replacement span should also be cleaned up when the surviving holder is ended." ) @@ -242,6 +242,7 @@ def test_duplicate_run_id_replaces_association_properties(handler): # Issue #3526 — orphaned context_api.attach() in on_chain_end corrupts context stack # --------------------------------------------------------------------------- + def test_on_chain_end_does_not_leak_context_frame(handler): """ Regression test for issue #3526. @@ -370,8 +371,7 @@ def test_duplicate_llm_run_id_replaces_association_properties(handler): handler._end_span(second_span, run_id) assert _association_properties() == {}, ( - "association_properties from the replacement LLM span should be cleaned up " - "when the surviving holder is ended." + "association_properties from the replacement LLM span should be cleaned up when the surviving holder is ended." ) assert not _suppression_active(), ( "Suppression must be cleared after ending the replacement LLM span, matching " diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_documents_chains.py b/packages/opentelemetry-instrumentation-langchain/tests/test_documents_chains.py index 74f02cac1d..0b8d07200a 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/test_documents_chains.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_documents_chains.py @@ -42,9 +42,7 @@ def test_sequential_chain(instrument_legacy, span_exporter, log_exporter): ] ) llm = ChatCohere(model="command-r-08-2024", temperature=0.75) - chain = load_summarize_chain(llm, chain_type="stuff").with_config( - run_name="stuff_chain" - ) + chain = load_summarize_chain(llm, chain_type="stuff").with_config(run_name="stuff_chain") chain.invoke(small_docs) spans = span_exporter.get_finished_spans() @@ -64,24 +62,18 @@ def test_sequential_chain(instrument_legacy, span_exporter, log_exporter): assert data["outputs"].keys() == {"output_text"} logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr -def test_sequential_chain_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +def test_sequential_chain_with_events_with_content(instrument_with_content, span_exporter, log_exporter): small_docs = CharacterTextSplitter().create_documents( texts=[ INPUT_TEXT, ] ) llm = ChatCohere(model="command-r-08-2024", temperature=0.75) - chain = load_summarize_chain(llm, chain_type="stuff").with_config( - run_name="stuff_chain" - ) + chain = load_summarize_chain(llm, chain_type="stuff").with_config(run_name="stuff_chain") response = chain.invoke(small_docs) spans = span_exporter.get_finished_spans() @@ -116,18 +108,14 @@ def test_sequential_chain_with_events_with_content( @pytest.mark.vcr -def test_sequential_chain_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +def test_sequential_chain_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): small_docs = CharacterTextSplitter().create_documents( texts=[ INPUT_TEXT, ] ) llm = ChatCohere(model="command-r-08-2024", temperature=0.75) - chain = load_summarize_chain(llm, chain_type="stuff").with_config( - run_name="stuff_chain" - ) + chain = load_summarize_chain(llm, chain_type="stuff").with_config(run_name="stuff_chain") chain.invoke(small_docs) spans = span_exporter.get_finished_spans() diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_finish_reasons.py b/packages/opentelemetry-instrumentation-langchain/tests/test_finish_reasons.py index ddfdda2354..93fe8797cd 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/test_finish_reasons.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_finish_reasons.py @@ -32,21 +32,25 @@ def set_attribute(key, value): # ---------- _map_finish_reason unit tests ---------- + class TestMapFinishReason: - @pytest.mark.parametrize("raw,expected", [ - ("stop", "stop"), - ("length", "length"), - ("tool_calls", "tool_call"), - ("function_call", "tool_call"), - ("content_filter", "content_filter"), - # Anthropic - ("end_turn", "stop"), - ("stop_sequence", "stop"), - ("tool_use", "tool_call"), - ("max_tokens", "length"), - # Unknown passthrough - ("some_future_reason", "some_future_reason"), - ]) + @pytest.mark.parametrize( + "raw,expected", + [ + ("stop", "stop"), + ("length", "length"), + ("tool_calls", "tool_call"), + ("function_call", "tool_call"), + ("content_filter", "content_filter"), + # Anthropic + ("end_turn", "stop"), + ("stop_sequence", "stop"), + ("tool_use", "tool_call"), + ("max_tokens", "length"), + # Unknown passthrough + ("some_future_reason", "some_future_reason"), + ], + ) def test_known_and_unknown_reasons(self, raw, expected): assert _map_finish_reason(raw) == expected @@ -59,6 +63,7 @@ def test_empty_string_returns_empty(self): # ---------- span-level finish_reasons tests ---------- + class TestFinishReasonsSpanAttribute: def _make_generation(self, content="OK", finish_reason=None): gen_info = {"finish_reason": finish_reason} if finish_reason else {} diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_generation_role_extraction.py b/packages/opentelemetry-instrumentation-langchain/tests/test_generation_role_extraction.py index af039edef9..4a4ed36a72 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/test_generation_role_extraction.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_generation_role_extraction.py @@ -35,10 +35,7 @@ def set_attribute(key, value): def test_chat_generation_with_ai_message_role(self, mock_span, monkeypatch): """Test that ChatGeneration with AIMessage correctly extracts 'assistant' role.""" # Mock should_send_prompts to return True - monkeypatch.setattr( - "opentelemetry.instrumentation.langchain.span_utils.should_send_prompts", - lambda: True - ) + monkeypatch.setattr("opentelemetry.instrumentation.langchain.span_utils.should_send_prompts", lambda: True) # Create ChatGeneration with AIMessage generation = ChatGeneration(message=AIMessage(content="Hello!")) @@ -55,15 +52,10 @@ def test_chat_generation_with_ai_message_role(self, mock_span, monkeypatch): def test_chat_generation_with_tool_message_role(self, mock_span, monkeypatch): """Test that ChatGeneration with ToolMessage correctly extracts 'tool' role.""" # Mock should_send_prompts to return True - monkeypatch.setattr( - "opentelemetry.instrumentation.langchain.span_utils.should_send_prompts", - lambda: True - ) + monkeypatch.setattr("opentelemetry.instrumentation.langchain.span_utils.should_send_prompts", lambda: True) # Create ChatGeneration with ToolMessage - generation = ChatGeneration( - message=ToolMessage(content="Tool result", tool_call_id="123") - ) + generation = ChatGeneration(message=ToolMessage(content="Tool result", tool_call_id="123")) llm_result = LLMResult(generations=[[generation]]) # Call the function @@ -77,10 +69,7 @@ def test_chat_generation_with_tool_message_role(self, mock_span, monkeypatch): def test_generation_without_message_defaults_to_assistant(self, mock_span, monkeypatch): """Test that Generation (non-chat) defaults to 'assistant' role.""" # Mock should_send_prompts to return True - monkeypatch.setattr( - "opentelemetry.instrumentation.langchain.span_utils.should_send_prompts", - lambda: True - ) + monkeypatch.setattr("opentelemetry.instrumentation.langchain.span_utils.should_send_prompts", lambda: True) # Create Generation without message (legacy completion) generation = Generation(text="This is a completion") @@ -97,10 +86,7 @@ def test_generation_without_message_defaults_to_assistant(self, mock_span, monke def test_multiple_generations_with_different_roles(self, mock_span, monkeypatch): """Test that multiple generations with different message types are handled correctly.""" # Mock should_send_prompts to return True - monkeypatch.setattr( - "opentelemetry.instrumentation.langchain.span_utils.should_send_prompts", - lambda: True - ) + monkeypatch.setattr("opentelemetry.instrumentation.langchain.span_utils.should_send_prompts", lambda: True) # Create multiple generations with different message types gen1 = ChatGeneration(message=AIMessage(content="AI response")) @@ -122,10 +108,7 @@ def test_multiple_generations_with_different_roles(self, mock_span, monkeypatch) def test_generation_type_attribute_is_not_used(self, mock_span, monkeypatch): """Test that generation.type (which returns class name) is not used directly.""" # Mock should_send_prompts to return True - monkeypatch.setattr( - "opentelemetry.instrumentation.langchain.span_utils.should_send_prompts", - lambda: True - ) + monkeypatch.setattr("opentelemetry.instrumentation.langchain.span_utils.should_send_prompts", lambda: True) # Create ChatGeneration - note that generation.type would be "ChatGeneration" generation = ChatGeneration(message=AIMessage(content="Test")) diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_langgraph.py b/packages/opentelemetry-instrumentation-langchain/tests/test_langgraph.py index 6e330e0e45..3fdc3982d6 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/test_langgraph.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_langgraph.py @@ -61,6 +61,7 @@ def calculate(state: State): # The openai_span comes from the OpenAI instrumentation (direct SDK call), # which now uses the parts-based JSON message format import json as _json + input_msgs = _json.loads(openai_span.attributes[GenAIAttributes.GEN_AI_INPUT_MESSAGES]) system_content = next(m for m in input_msgs if m["role"] == "system")["parts"][0]["content"] assert system_content == "You are a mathematician." @@ -328,9 +329,7 @@ async def run_test_agent(): for span in spans: parent_name = "None" if span.parent: - parent_span = next( - (s for s in spans if s.context.span_id == span.parent.span_id), None - ) + parent_span = next((s for s in spans if s.context.span_id == span.parent.span_id), None) if parent_span: parent_name = parent_span.name else: @@ -362,17 +361,15 @@ async def run_test_agent(): print("\nHierarchy check:") print(f"POST parent: {post_span.parent.span_id if post_span.parent else 'None'}") print(f"execute_task http_call ID: {http_call_task_span.context.span_id}") - print( - f"test_agent_span parent: {test_agent_span.parent.span_id if test_agent_span.parent else 'None'}" - ) + print(f"test_agent_span parent: {test_agent_span.parent.span_id if test_agent_span.parent else 'None'}") print(f"execute_task otel_span ID: {otel_span_task_span.context.span_id}") - assert ( - post_span.parent.span_id == http_call_task_span.context.span_id - ), "POST span should be child of execute_task http_call span" - assert ( - test_agent_span.parent.span_id == otel_span_task_span.context.span_id - ), "test_agent_span should be child of execute_task otel_span span" + assert post_span.parent.span_id == http_call_task_span.context.span_id, ( + "POST span should be child of execute_task http_call span" + ) + assert test_agent_span.parent.span_id == otel_span_task_span.context.span_id, ( + "test_agent_span should be child of execute_task otel_span span" + ) assert http_call_task_span.parent.span_id == workflow_span.context.span_id assert otel_span_task_span.parent.span_id == workflow_span.context.span_id @@ -380,9 +377,7 @@ async def run_test_agent(): assert graph_span.parent.span_id == root_span.context.span_id -def test_context_detachment_error_handling( - instrument_legacy, span_exporter, tracer_provider, caplog -): +def test_context_detachment_error_handling(instrument_legacy, span_exporter, tracer_provider, caplog): """ Test that context detachment errors are handled properly without logging. @@ -436,9 +431,7 @@ async def parallel_task(task_id: int): tasks = [parallel_task(i) for i in range(5)] parallel_results = await asyncio.gather(*tasks) - combined_result = ( - f"{state['result']} + parallel_results: {','.join(parallel_results)}" - ) + combined_result = f"{state['result']} + parallel_results: {','.join(parallel_results)}" return {"counter": state["counter"], "result": combined_result} def build_context_stress_graph(): @@ -482,27 +475,13 @@ async def run_concurrent_executions(): nested_spans = [s for s in spans if s.name == "nested_span"] parallel_task_spans = [s for s in spans if s.name.startswith("parallel_task_")] - assert ( - len(workflow_spans) == 10 - ), f"Expected 10 workflow spans, got {len(workflow_spans)}" - assert ( - len(concurrent_spans) == 10 - ), f"Expected 10 concurrent spans, got {len(concurrent_spans)}" - assert ( - len(nested_spans) == 10 - ), f"Expected 10 nested spans, got {len(nested_spans)}" - assert ( - len(parallel_task_spans) == 50 - ), f"Expected 50 parallel task spans, got {len(parallel_task_spans)}" - - error_logs = [ - record.message - for record in caplog.records - if record.levelno >= logging.ERROR - ] - context_errors = [ - msg for msg in error_logs if "Failed to detach context" in msg - ] + assert len(workflow_spans) == 10, f"Expected 10 workflow spans, got {len(workflow_spans)}" + assert len(concurrent_spans) == 10, f"Expected 10 concurrent spans, got {len(concurrent_spans)}" + assert len(nested_spans) == 10, f"Expected 10 nested spans, got {len(nested_spans)}" + assert len(parallel_task_spans) == 50, f"Expected 50 parallel task spans, got {len(parallel_task_spans)}" + + error_logs = [record.message for record in caplog.records if record.levelno >= logging.ERROR] + context_errors = [msg for msg in error_logs if "Failed to detach context" in msg] assert len(context_errors) == 0, ( f"Found {len(context_errors)} context detachment errors in logs. " @@ -516,9 +495,7 @@ async def run_concurrent_executions(): None, ) assert parent_span is not None, "Parent span should exist" - assert ( - parent_span.name == "concurrent_async_span" - ), "Nested span should be child of concurrent_async_span" + assert parent_span.name == "concurrent_async_span", "Nested span should be child of concurrent_async_span" def test_create_react_agent_span(instrument_legacy, span_exporter): @@ -562,9 +539,7 @@ def test_retriever_span_attributes(instrument_legacy, span_exporter): from langchain_core.retrievers import BaseRetriever class MockRetriever(BaseRetriever): - def _get_relevant_documents( - self, query: str, *, run_manager: CallbackManagerForRetrieverRun - ) -> List[Document]: + def _get_relevant_documents(self, query: str, *, run_manager: CallbackManagerForRetrieverRun) -> List[Document]: return [Document(page_content="Test", metadata={"source": "test.txt"})] MockRetriever().invoke("test query") @@ -593,8 +568,7 @@ def before_model(self, state, runtime): middleware_span = next(s for s in spans if "TestMiddleware" in s.name) assert ( - middleware_span.attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] - == GenAICustomOperationName.EXECUTE_TASK.value + middleware_span.attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] == GenAICustomOperationName.EXECUTE_TASK.value ) assert middleware_span.attributes[SpanAttributes.GEN_AI_TASK_KIND] == "TestMiddleware" assert middleware_span.attributes[SpanAttributes.GEN_AI_TASK_STATUS] == "success" @@ -731,7 +705,7 @@ def get_info(query: str) -> str: model=MockChatModel(), tools=[get_info], name="PromptAgent", - prompt="You are a helpful assistant that provides accurate information." + prompt="You are a helpful assistant that provides accurate information.", ) spans = span_exporter.get_finished_spans() @@ -759,8 +733,7 @@ async def abefore_model(self, state, runtime): assert len(middleware_spans) >= 1 middleware_span = middleware_spans[0] assert ( - middleware_span.attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] - == GenAICustomOperationName.EXECUTE_TASK.value + middleware_span.attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] == GenAICustomOperationName.EXECUTE_TASK.value ) assert middleware_span.attributes[SpanAttributes.GEN_AI_TASK_KIND] == "AsyncTestMiddleware" @@ -814,9 +787,7 @@ def _llm_type(self) -> str: return "mock" def _generate(self, messages, stop=None, run_manager=None, **kwargs): - return ChatResult( - generations=[ChatGeneration(message=AIMessage(content="Mock"))] - ) + return ChatResult(generations=[ChatGeneration(message=AIMessage(content="Mock"))]) def bind_tools(self, tools, **kwargs): return self @@ -834,22 +805,15 @@ def get_time(timezone: str) -> str: tool_node = ToolNode([get_weather, get_time]) # Before the fix this raised: TypeError: 'ToolNode' object is not iterable - _ = create_react_agent( - model=MockChatModel(), tools=tool_node, name="ToolNodeAgent" - ) + _ = create_react_agent(model=MockChatModel(), tools=tool_node, name="ToolNodeAgent") spans = span_exporter.get_finished_spans() create_span = next(s for s in spans if "create_agent" in s.name) - assert ( - create_span.attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] - == GenAiOperationNameValues.CREATE_AGENT.value - ) + assert create_span.attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] == GenAiOperationNameValues.CREATE_AGENT.value assert create_span.attributes[GenAIAttributes.GEN_AI_AGENT_NAME] == "ToolNodeAgent" assert GenAIAttributes.GEN_AI_TOOL_DEFINITIONS in create_span.attributes - tool_defs = json.loads( - create_span.attributes[GenAIAttributes.GEN_AI_TOOL_DEFINITIONS] - ) + tool_defs = json.loads(create_span.attributes[GenAIAttributes.GEN_AI_TOOL_DEFINITIONS]) tool_names = {td["name"] for td in tool_defs} assert tool_names == {"get_weather", "get_time"} diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_lcel.py b/packages/opentelemetry-instrumentation-langchain/tests/test_lcel.py index 0ae5b189c4..d91245fb86 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/test_lcel.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_lcel.py @@ -27,15 +27,13 @@ class Joke(BaseModel): openai_functions = [convert_pydantic_to_openai_function(Joke)] - prompt = ChatPromptTemplate.from_messages( - [("system", "You are helpful assistant"), ("user", "{input}")] - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are helpful assistant"), ("user", "{input}")]) model = ChatOpenAI(model="gpt-3.5-turbo") output_parser = JsonOutputFunctionsParser() - chain = ( - prompt | model.bind(functions=openai_functions) | output_parser - ).with_config({"run_name": "ThisIsATestChain", "tags": ["test_tag"]}) + chain = (prompt | model.bind(functions=openai_functions) | output_parser).with_config( + {"run_name": "ThisIsATestChain", "tags": ["test_tag"]} + ) chain.invoke({"input": "tell me a short joke"}) spans = span_exporter.get_finished_spans() @@ -49,43 +47,29 @@ class Joke(BaseModel): ] ) == set([span.name for span in spans]) - workflow_span = next( - span for span in spans if span.name == "ThisIsATestChain.workflow" - ) - prompt_task_span = next( - span for span in spans if span.name == "execute_task ChatPromptTemplate" - ) - chat_openai_task_span = next( - span for span in spans if span.name == "ChatOpenAI.chat" - ) - output_parser_task_span = next( - span for span in spans if span.name == "execute_task JsonOutputFunctionsParser" - ) + workflow_span = next(span for span in spans if span.name == "ThisIsATestChain.workflow") + prompt_task_span = next(span for span in spans if span.name == "execute_task ChatPromptTemplate") + chat_openai_task_span = next(span for span in spans if span.name == "ChatOpenAI.chat") + output_parser_task_span = next(span for span in spans if span.name == "execute_task JsonOutputFunctionsParser") assert prompt_task_span.parent.span_id == workflow_span.context.span_id assert chat_openai_task_span.parent.span_id == workflow_span.context.span_id assert output_parser_task_span.parent.span_id == workflow_span.context.span_id - assert json.loads( - workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT] - ) == { + assert json.loads(workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT]) == { "inputs": {"input": "tell me a short joke"}, "tags": ["test_tag"], "metadata": {}, "kwargs": {"name": "ThisIsATestChain"}, } - assert json.loads( - workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT] - ) == { + assert json.loads(workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT]) == { "outputs": { "setup": "Why couldn't the bicycle stand up by itself?", "punchline": "It was two tired!", }, "kwargs": {"tags": ["test_tag"]}, } - assert json.loads( - prompt_task_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT] - ) == { + assert json.loads(prompt_task_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT]) == { "inputs": {"input": "tell me a short joke"}, "tags": ["seq:step:1", "test_tag"], "metadata": {}, @@ -94,9 +78,7 @@ class Joke(BaseModel): "name": "ChatPromptTemplate", }, } - assert json.loads( - prompt_task_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT] - ) == { + assert json.loads(prompt_task_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT]) == { "kwargs": {"tags": ["seq:step:1", "test_tag"]}, "outputs": { "id": ["langchain", "prompts", "chat", "ChatPromptValue"], @@ -105,7 +87,7 @@ class Joke(BaseModel): { "id": ["langchain", "schema", "messages", "SystemMessage"], "kwargs": { - "content": "You are helpful " "assistant", + "content": "You are helpful assistant", "type": "system", }, "lc": 1, @@ -114,7 +96,7 @@ class Joke(BaseModel): { "id": ["langchain", "schema", "messages", "HumanMessage"], "kwargs": { - "content": "tell me a short " "joke", + "content": "tell me a short joke", "type": "human", }, "lc": 1, @@ -128,15 +110,11 @@ class Joke(BaseModel): } logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr -def test_simple_lcel_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +def test_simple_lcel_with_events_with_content(instrument_with_content, span_exporter, log_exporter): class Joke(BaseModel): """Joke to tell user.""" @@ -145,15 +123,13 @@ class Joke(BaseModel): openai_functions = [convert_pydantic_to_openai_function(Joke)] - prompt = ChatPromptTemplate.from_messages( - [("system", "You are helpful assistant"), ("user", "{input}")] - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are helpful assistant"), ("user", "{input}")]) model = ChatOpenAI(model="gpt-3.5-turbo") output_parser = JsonOutputFunctionsParser() - chain = ( - prompt | model.bind(functions=openai_functions) | output_parser - ).with_config({"run_name": "ThisIsATestChain", "tags": ["test_tag"]}) + chain = (prompt | model.bind(functions=openai_functions) | output_parser).with_config( + {"run_name": "ThisIsATestChain", "tags": ["test_tag"]} + ) chain.invoke({"input": "tell me a short joke"}) spans = span_exporter.get_finished_spans() @@ -167,18 +143,10 @@ class Joke(BaseModel): ] ) == set([span.name for span in spans]) - workflow_span = next( - span for span in spans if span.name == "ThisIsATestChain.workflow" - ) - prompt_task_span = next( - span for span in spans if span.name == "execute_task ChatPromptTemplate" - ) - chat_openai_task_span = next( - span for span in spans if span.name == "ChatOpenAI.chat" - ) - output_parser_task_span = next( - span for span in spans if span.name == "execute_task JsonOutputFunctionsParser" - ) + workflow_span = next(span for span in spans if span.name == "ThisIsATestChain.workflow") + prompt_task_span = next(span for span in spans if span.name == "execute_task ChatPromptTemplate") + chat_openai_task_span = next(span for span in spans if span.name == "ChatOpenAI.chat") + output_parser_task_span = next(span for span in spans if span.name == "execute_task JsonOutputFunctionsParser") assert prompt_task_span.parent.span_id == workflow_span.context.span_id assert chat_openai_task_span.parent.span_id == workflow_span.context.span_id @@ -188,14 +156,10 @@ class Joke(BaseModel): assert len(logs) == 3 # Validate system message Event - assert_message_in_logs( - logs[0], "gen_ai.system.message", {"content": "You are helpful assistant"} - ) + assert_message_in_logs(logs[0], "gen_ai.system.message", {"content": "You are helpful assistant"}) # Validate user message Event - assert_message_in_logs( - logs[1], "gen_ai.user.message", {"content": "tell me a short joke"} - ) + assert_message_in_logs(logs[1], "gen_ai.user.message", {"content": "tell me a short joke"}) # Validate AI choice Event _choice_event = { @@ -218,9 +182,7 @@ class Joke(BaseModel): @pytest.mark.vcr -def test_simple_lcel_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +def test_simple_lcel_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): class Joke(BaseModel): """Joke to tell user.""" @@ -229,15 +191,13 @@ class Joke(BaseModel): openai_functions = [convert_pydantic_to_openai_function(Joke)] - prompt = ChatPromptTemplate.from_messages( - [("system", "You are helpful assistant"), ("user", "{input}")] - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are helpful assistant"), ("user", "{input}")]) model = ChatOpenAI(model="gpt-3.5-turbo") output_parser = JsonOutputFunctionsParser() - chain = ( - prompt | model.bind(functions=openai_functions) | output_parser - ).with_config({"run_name": "ThisIsATestChain", "tags": ["test_tag"]}) + chain = (prompt | model.bind(functions=openai_functions) | output_parser).with_config( + {"run_name": "ThisIsATestChain", "tags": ["test_tag"]} + ) chain.invoke({"input": "tell me a short joke"}) spans = span_exporter.get_finished_spans() @@ -251,18 +211,10 @@ class Joke(BaseModel): ] ) == set([span.name for span in spans]) - workflow_span = next( - span for span in spans if span.name == "ThisIsATestChain.workflow" - ) - prompt_task_span = next( - span for span in spans if span.name == "execute_task ChatPromptTemplate" - ) - chat_openai_task_span = next( - span for span in spans if span.name == "ChatOpenAI.chat" - ) - output_parser_task_span = next( - span for span in spans if span.name == "execute_task JsonOutputFunctionsParser" - ) + workflow_span = next(span for span in spans if span.name == "ThisIsATestChain.workflow") + prompt_task_span = next(span for span in spans if span.name == "execute_task ChatPromptTemplate") + chat_openai_task_span = next(span for span in spans if span.name == "ChatOpenAI.chat") + output_parser_task_span = next(span for span in spans if span.name == "execute_task JsonOutputFunctionsParser") assert prompt_task_span.parent.span_id == workflow_span.context.span_id assert chat_openai_task_span.parent.span_id == workflow_span.context.span_id @@ -295,9 +247,7 @@ async def test_async_lcel(instrument_legacy, span_exporter, log_exporter): temperature=0, ) - prompt = PromptTemplate.from_template( - "write 10 lines of random text about ${product}" - ) + prompt = PromptTemplate.from_template("write 10 lines of random text about ${product}") runnable = prompt | chat | StrOutputParser() response = await runnable.ainvoke({"product": "colorful socks"}) @@ -310,45 +260,31 @@ async def test_async_lcel(instrument_legacy, span_exporter, log_exporter): "RunnableSequence.workflow", } == set([span.name for span in spans]) - workflow_span = next( - span for span in spans if span.name == "RunnableSequence.workflow" - ) - chat_openai_task_span = next( - span for span in spans if span.name == "ChatOpenAI.chat" - ) - output_parser_task_span = next( - span for span in spans if span.name == "execute_task StrOutputParser" - ) + workflow_span = next(span for span in spans if span.name == "RunnableSequence.workflow") + chat_openai_task_span = next(span for span in spans if span.name == "ChatOpenAI.chat") + output_parser_task_span = next(span for span in spans if span.name == "execute_task StrOutputParser") assert chat_openai_task_span.parent.span_id == workflow_span.context.span_id assert output_parser_task_span.parent.span_id == workflow_span.context.span_id - assert json.loads( - workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT] - ) == { + assert json.loads(workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT]) == { "inputs": {"product": "colorful socks"}, "tags": [], "metadata": {}, "kwargs": {"name": "RunnableSequence"}, } - assert json.loads( - workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT] - ) == { + assert json.loads(workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT]) == { "outputs": response, "kwargs": {"tags": []}, } logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr @pytest.mark.asyncio -async def test_async_lcel_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +async def test_async_lcel_with_events_with_content(instrument_with_content, span_exporter, log_exporter): chat = ChatOpenAI( model="gpt-4", temperature=0, @@ -368,15 +304,9 @@ async def test_async_lcel_with_events_with_content( "RunnableSequence.workflow", } == set([span.name for span in spans]) - workflow_span = next( - span for span in spans if span.name == "RunnableSequence.workflow" - ) - chat_openai_task_span = next( - span for span in spans if span.name == "ChatOpenAI.chat" - ) - output_parser_task_span = next( - span for span in spans if span.name == "execute_task StrOutputParser" - ) + workflow_span = next(span for span in spans if span.name == "RunnableSequence.workflow") + chat_openai_task_span = next(span for span in spans if span.name == "ChatOpenAI.chat") + output_parser_task_span = next(span for span in spans if span.name == "execute_task StrOutputParser") assert chat_openai_task_span.parent.span_id == workflow_span.context.span_id assert output_parser_task_span.parent.span_id == workflow_span.context.span_id @@ -404,17 +334,13 @@ async def test_async_lcel_with_events_with_content( @pytest.mark.vcr @pytest.mark.asyncio -async def test_async_lcel_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +async def test_async_lcel_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): chat = ChatOpenAI( model="gpt-4", temperature=0, ) - prompt = PromptTemplate.from_template( - "write 10 lines of random text about ${product}" - ) + prompt = PromptTemplate.from_template("write 10 lines of random text about ${product}") runnable = prompt | chat | StrOutputParser() await runnable.ainvoke({"product": "colorful socks"}) @@ -427,15 +353,9 @@ async def test_async_lcel_with_events_with_no_content( "RunnableSequence.workflow", } == set([span.name for span in spans]) - workflow_span = next( - span for span in spans if span.name == "RunnableSequence.workflow" - ) - chat_openai_task_span = next( - span for span in spans if span.name == "ChatOpenAI.chat" - ) - output_parser_task_span = next( - span for span in spans if span.name == "execute_task StrOutputParser" - ) + workflow_span = next(span for span in spans if span.name == "RunnableSequence.workflow") + chat_openai_task_span = next(span for span in spans if span.name == "ChatOpenAI.chat") + output_parser_task_span = next(span for span in spans if span.name == "execute_task StrOutputParser") assert chat_openai_task_span.parent.span_id == workflow_span.context.span_id assert output_parser_task_span.parent.span_id == workflow_span.context.span_id @@ -463,9 +383,7 @@ def test_invoke(instrument_legacy, span_exporter, log_exporter): streaming=True, ) - prompt = PromptTemplate.from_template( - "write 10 lines of random text about ${product}" - ) + prompt = PromptTemplate.from_template("write 10 lines of random text about ${product}") runnable = prompt | chat | StrOutputParser() runnable.invoke({"product": "colorful socks"}) @@ -479,15 +397,11 @@ def test_invoke(instrument_legacy, span_exporter, log_exporter): ] == [span.name for span in spans] logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr -def test_invoke_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +def test_invoke_with_events_with_content(instrument_with_content, span_exporter, log_exporter): chat = ChatOpenAI( model="gpt-4", temperature=0, @@ -528,18 +442,14 @@ def test_invoke_with_events_with_content( @pytest.mark.vcr -def test_invoke_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +def test_invoke_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): chat = ChatOpenAI( model="gpt-4", temperature=0, streaming=True, ) - prompt = PromptTemplate.from_template( - "write 10 lines of random text about ${product}" - ) + prompt = PromptTemplate.from_template("write 10 lines of random text about ${product}") runnable = prompt | chat | StrOutputParser() runnable.invoke({"product": "colorful socks"}) @@ -574,9 +484,7 @@ def test_stream(instrument_legacy, span_exporter, log_exporter): temperature=0, ) - prompt = PromptTemplate.from_template( - "write 10 lines of random text about ${product}" - ) + prompt = PromptTemplate.from_template("write 10 lines of random text about ${product}") runnable = prompt | chat | StrOutputParser() res = runnable.stream( input={"product": "colorful socks"}, @@ -595,15 +503,11 @@ def test_stream(instrument_legacy, span_exporter, log_exporter): ] == [span.name for span in spans] logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr -def test_stream_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +def test_stream_with_events_with_content(instrument_with_content, span_exporter, log_exporter): chat = ChatOpenAI( model="gpt-4", temperature=0, @@ -647,17 +551,13 @@ def test_stream_with_events_with_content( @pytest.mark.vcr -def test_stream_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +def test_stream_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): chat = ChatOpenAI( model="gpt-4", temperature=0, ) - prompt = PromptTemplate.from_template( - "write 10 lines of random text about ${product}" - ) + prompt = PromptTemplate.from_template("write 10 lines of random text about ${product}") runnable = prompt | chat | StrOutputParser() res = runnable.stream( input={"product": "colorful socks"}, @@ -699,9 +599,7 @@ async def test_async_invoke(instrument_legacy, span_exporter, log_exporter): streaming=True, ) - prompt = PromptTemplate.from_template( - "write 10 lines of random text about ${product}" - ) + prompt = PromptTemplate.from_template("write 10 lines of random text about ${product}") runnable = prompt | chat | StrOutputParser() await runnable.ainvoke({"product": "colorful socks"}) @@ -715,16 +613,12 @@ async def test_async_invoke(instrument_legacy, span_exporter, log_exporter): ] == [span.name for span in spans] logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr @pytest.mark.asyncio -async def test_async_invoke_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +async def test_async_invoke_with_events_with_content(instrument_with_content, span_exporter, log_exporter): chat = ChatOpenAI( model="gpt-4", temperature=0, @@ -766,18 +660,14 @@ async def test_async_invoke_with_events_with_content( @pytest.mark.vcr @pytest.mark.asyncio -async def test_async_invoke_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +async def test_async_invoke_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): chat = ChatOpenAI( model="gpt-4", temperature=0, streaming=True, ) - prompt = PromptTemplate.from_template( - "write 10 lines of random text about ${product}" - ) + prompt = PromptTemplate.from_template("write 10 lines of random text about ${product}") runnable = prompt | chat | StrOutputParser() await runnable.ainvoke({"product": "colorful socks"}) @@ -817,15 +707,11 @@ class Joke(BaseModel): openai_functions = [convert_pydantic_to_openai_function(Joke)] - prompt = ChatPromptTemplate.from_messages( - [("system", "You are helpful assistant"), ("user", "{input}")] - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are helpful assistant"), ("user", "{input}")]) model = ChatOpenAI(model="gpt-3.5-turbo") output_parser = JsonOutputFunctionsParser() - chain = ( - prompt | model.bind(functions=openai_functions) | output_parser - ).with_config( + chain = (prompt | model.bind(functions=openai_functions) | output_parser).with_config( { "run_name": "DateTimeTestChain", "tags": ["datetime_test"], @@ -837,13 +723,9 @@ class Joke(BaseModel): spans = span_exporter.get_finished_spans() - workflow_span = next( - span for span in spans if span.name == "DateTimeTestChain.workflow" - ) + workflow_span = next(span for span in spans if span.name == "DateTimeTestChain.workflow") - entity_input = json.loads( - workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT] - ) + entity_input = json.loads(workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT]) assert entity_input["metadata"]["timestamp"] == "2023-05-17T12:34:56" assert entity_input["metadata"]["test_name"] == "datetime_test" @@ -858,15 +740,11 @@ class Joke(BaseModel): ) == set([span.name for span in spans]) logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr -def test_lcel_with_datetime_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +def test_lcel_with_datetime_with_events_with_content(instrument_with_content, span_exporter, log_exporter): test_date = datetime.datetime(2023, 5, 17, 12, 34, 56) class Joke(BaseModel): @@ -877,15 +755,11 @@ class Joke(BaseModel): openai_functions = [convert_pydantic_to_openai_function(Joke)] - prompt = ChatPromptTemplate.from_messages( - [("system", "You are helpful assistant"), ("user", "{input}")] - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are helpful assistant"), ("user", "{input}")]) model = ChatOpenAI(model="gpt-3.5-turbo") output_parser = JsonOutputFunctionsParser() - chain = ( - prompt | model.bind(functions=openai_functions) | output_parser - ).with_config( + chain = (prompt | model.bind(functions=openai_functions) | output_parser).with_config( { "run_name": "DateTimeTestChain", "tags": ["datetime_test"], @@ -910,14 +784,10 @@ class Joke(BaseModel): assert len(logs) == 3 # Validate system message Event - assert_message_in_logs( - logs[0], "gen_ai.system.message", {"content": "You are helpful assistant"} - ) + assert_message_in_logs(logs[0], "gen_ai.system.message", {"content": "You are helpful assistant"}) # Validate user message Event - assert_message_in_logs( - logs[1], "gen_ai.user.message", {"content": "tell me a short joke"} - ) + assert_message_in_logs(logs[1], "gen_ai.user.message", {"content": "tell me a short joke"}) # Validate AI choice Event _choice_event = { @@ -940,9 +810,7 @@ class Joke(BaseModel): @pytest.mark.vcr -def test_lcel_with_datetime_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +def test_lcel_with_datetime_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): test_date = datetime.datetime(2023, 5, 17, 12, 34, 56) class Joke(BaseModel): @@ -953,15 +821,11 @@ class Joke(BaseModel): openai_functions = [convert_pydantic_to_openai_function(Joke)] - prompt = ChatPromptTemplate.from_messages( - [("system", "You are helpful assistant"), ("user", "{input}")] - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are helpful assistant"), ("user", "{input}")]) model = ChatOpenAI(model="gpt-3.5-turbo") output_parser = JsonOutputFunctionsParser() - chain = ( - prompt | model.bind(functions=openai_functions) | output_parser - ).with_config( + chain = (prompt | model.bind(functions=openai_functions) | output_parser).with_config( { "run_name": "DateTimeTestChain", "tags": ["datetime_test"], diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_llms.py b/packages/opentelemetry-instrumentation-langchain/tests/test_llms.py index 03d566d178..ed40e23128 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/test_llms.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_llms.py @@ -125,9 +125,7 @@ def open_ai_prompt(): @pytest.mark.vcr @pytest.mark.skipif(not HAS_TEXT_GENERATION, reason="text_generation not installed") def test_custom_llm(instrument_legacy, span_exporter, log_exporter): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant"), ("user", "{input}")] - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"), ("user", "{input}")]) model = HuggingFaceTextGenInference( inference_server_url="https://w8qtunpthvh1r7a0.us-east-1.aws.endpoints.huggingface.cloud" ) @@ -143,37 +141,25 @@ def test_custom_llm(instrument_legacy, span_exporter, log_exporter): "RunnableSequence.workflow", ] == [span.name for span in spans] - hugging_face_span = next( - span for span in spans if span.name == "HuggingFaceTextGenInference.completion" - ) + hugging_face_span = next(span for span in spans if span.name == "HuggingFaceTextGenInference.completion") assert hugging_face_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == "unknown" assert hugging_face_span.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] == "hugging_face" input_messages = json.loads(hugging_face_span.attributes[GenAIAttributes.GEN_AI_INPUT_MESSAGES]) assert ( - input_messages[0]["parts"][0]["content"] - == "System: You are a helpful assistant\nHuman: tell me a short joke" + input_messages[0]["parts"][0]["content"] == "System: You are a helpful assistant\nHuman: tell me a short joke" ) output_messages = json.loads(hugging_face_span.attributes[GenAIAttributes.GEN_AI_OUTPUT_MESSAGES]) - assert ( - output_messages[0]["parts"][0]["content"] - == response - ) + assert output_messages[0]["parts"][0]["content"] == response logs = log_exporter.get_finished_logs() - assert len(logs) == 0, ( - "Assert that it doesn't emit logs when use_legacy_attributes is True" - ) + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr @pytest.mark.skipif(not HAS_TEXT_GENERATION, reason="text_generation not installed") -def test_custom_llm_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant"), ("user", "{input}")] - ) +def test_custom_llm_with_events_with_content(instrument_with_content, span_exporter, log_exporter): + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"), ("user", "{input}")]) model = HuggingFaceTextGenInference( inference_server_url="https://w8qtunpthvh1r7a0.us-east-1.aws.endpoints.huggingface.cloud" ) @@ -189,9 +175,7 @@ def test_custom_llm_with_events_with_content( "RunnableSequence.workflow", ] == [span.name for span in spans] - hugging_face_span = next( - span for span in spans if span.name == "HuggingFaceTextGenInference.completion" - ) + hugging_face_span = next(span for span in spans if span.name == "HuggingFaceTextGenInference.completion") assert hugging_face_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == "unknown" @@ -218,12 +202,8 @@ def test_custom_llm_with_events_with_content( @pytest.mark.vcr @pytest.mark.skipif(not HAS_TEXT_GENERATION, reason="text_generation not installed") -def test_custom_llm_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant"), ("user", "{input}")] - ) +def test_custom_llm_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"), ("user", "{input}")]) model = HuggingFaceTextGenInference( inference_server_url="https://w8qtunpthvh1r7a0.us-east-1.aws.endpoints.huggingface.cloud" ) @@ -239,9 +219,7 @@ def test_custom_llm_with_events_with_no_content( "RunnableSequence.workflow", ] == [span.name for span in spans] - hugging_face_span = next( - span for span in spans if span.name == "HuggingFaceTextGenInference.completion" - ) + hugging_face_span = next(span for span in spans if span.name == "HuggingFaceTextGenInference.completion") assert hugging_face_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == "unknown" @@ -262,9 +240,7 @@ def test_custom_llm_with_events_with_no_content( @pytest.mark.vcr def test_openai(instrument_legacy, span_exporter, log_exporter): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant"), ("human", "{input}")] - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"), ("human", "{input}")]) model = ChatOpenAI(model="gpt-4o-mini") chain = prompt | model @@ -294,29 +270,19 @@ def test_openai(instrument_legacy, span_exporter, log_exporter): assert openai_span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] == 1037 assert openai_span.attributes[SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS] == 2534 - workflow_span = next( - span for span in spans if span.name == "RunnableSequence.workflow" - ) - output = json.loads( - workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT] - ) + workflow_span = next(span for span in spans if span.name == "RunnableSequence.workflow") + output = json.loads(workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT]) # Validate the completion content via workflow output assert output["outputs"]["kwargs"]["content"] == response.content assert output["outputs"]["kwargs"]["type"] == "ai" logs = log_exporter.get_finished_logs() - assert len(logs) == 0, ( - "Assert that it doesn't emit logs when use_legacy_attributes is True" - ) + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr -def test_openai_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant"), ("human", "{input}")] - ) +def test_openai_with_events_with_content(instrument_with_content, span_exporter, log_exporter): + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"), ("human", "{input}")]) model = ChatOpenAI(model="gpt-4o-mini") chain = prompt | model @@ -344,9 +310,7 @@ def test_openai_with_events_with_content( assert len(logs) == 3 # Validate system message Event - assert_message_in_logs( - logs[0], "gen_ai.system.message", {"content": "You are a helpful assistant"} - ) + assert_message_in_logs(logs[0], "gen_ai.system.message", {"content": "You are a helpful assistant"}) # Validate user message Event assert_message_in_logs(logs[1], "gen_ai.user.message", {"content": prompt}) @@ -361,12 +325,8 @@ def test_openai_with_events_with_content( @pytest.mark.vcr -def test_openai_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant"), ("human", "{input}")] - ) +def test_openai_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"), ("human", "{input}")]) model = ChatOpenAI(model="gpt-4o-mini") chain = prompt | model @@ -418,9 +378,7 @@ class Joke(BaseModel): openai_functions = [convert_pydantic_to_openai_function(Joke)] - prompt = ChatPromptTemplate.from_messages( - [("system", "You are helpful assistant"), ("user", "{input}")] - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are helpful assistant"), ("user", "{input}")]) model = ChatOpenAI(model="gpt-3.5-turbo") output_parser = JsonOutputFunctionsParser() @@ -464,25 +422,17 @@ class Joke(BaseModel): assert openai_span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] == 35 assert openai_span.attributes[SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS] == 111 - workflow_span = next( - span for span in spans if span.name == "RunnableSequence.workflow" - ) - output = json.loads( - workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT] - ) + workflow_span = next(span for span in spans if span.name == "RunnableSequence.workflow") + output = json.loads(workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT]) # Validate the tool call via workflow output assert output["outputs"] == response logs = log_exporter.get_finished_logs() - assert len(logs) == 0, ( - "Assert that it doesn't emit logs when use_legacy_attributes is True" - ) + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr -def test_openai_functions_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +def test_openai_functions_with_events_with_content(instrument_with_content, span_exporter, log_exporter): class Joke(BaseModel): """Joke to tell user.""" @@ -491,9 +441,7 @@ class Joke(BaseModel): openai_functions = [convert_pydantic_to_openai_function(Joke)] - prompt = ChatPromptTemplate.from_messages( - [("system", "You are helpful assistant"), ("user", "{input}")] - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are helpful assistant"), ("user", "{input}")]) model = ChatOpenAI(model="gpt-3.5-turbo") output_parser = JsonOutputFunctionsParser() @@ -523,14 +471,10 @@ class Joke(BaseModel): assert len(logs) == 3 # Validate system message Event - assert_message_in_logs( - logs[0], "gen_ai.system.message", {"content": "You are helpful assistant"} - ) + assert_message_in_logs(logs[0], "gen_ai.system.message", {"content": "You are helpful assistant"}) # Validate user message Event - assert_message_in_logs( - logs[1], "gen_ai.user.message", {"content": "tell me a short joke"} - ) + assert_message_in_logs(logs[1], "gen_ai.user.message", {"content": "tell me a short joke"}) # Validate AI choice Event choice_event = { @@ -552,9 +496,7 @@ class Joke(BaseModel): @pytest.mark.vcr -def test_openai_functions_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +def test_openai_functions_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): class Joke(BaseModel): """Joke to tell user.""" @@ -563,9 +505,7 @@ class Joke(BaseModel): openai_functions = [convert_pydantic_to_openai_function(Joke)] - prompt = ChatPromptTemplate.from_messages( - [("system", "You are helpful assistant"), ("user", "{input}")] - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are helpful assistant"), ("user", "{input}")]) model = ChatOpenAI(model="gpt-3.5-turbo") output_parser = JsonOutputFunctionsParser() @@ -612,9 +552,7 @@ class Joke(BaseModel): @pytest.mark.vcr def test_anthropic(instrument_legacy, span_exporter, log_exporter): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant"), ("user", "{input}")] - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"), ("user", "{input}")]) model = ChatAnthropic(model="claude-2.1", temperature=0.5) chain = prompt | model @@ -629,9 +567,7 @@ def test_anthropic(instrument_legacy, span_exporter, log_exporter): ] == [span.name for span in spans] anthropic_span = next(span for span in spans if span.name == "ChatAnthropic.chat") - workflow_span = next( - span for span in spans if span.name == "RunnableSequence.workflow" - ) + workflow_span = next(span for span in spans if span.name == "RunnableSequence.workflow") assert anthropic_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == "claude-2.1" assert anthropic_span.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] == "anthropic" @@ -644,13 +580,8 @@ def test_anthropic(instrument_legacy, span_exporter, log_exporter): assert anthropic_span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS] == 19 assert anthropic_span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] == 22 assert anthropic_span.attributes[SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS] == 41 - assert ( - anthropic_span.attributes[GenAIAttributes.GEN_AI_RESPONSE_ID] - == "msg_017fMG9SRDFTBhcD1ibtN1nK" - ) - output = json.loads( - workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT] - ) + assert anthropic_span.attributes[GenAIAttributes.GEN_AI_RESPONSE_ID] == "msg_017fMG9SRDFTBhcD1ibtN1nK" + output = json.loads(workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT]) # We check essential fields instead of exact match due to library version differences output_kwargs = output["outputs"]["kwargs"] assert output_kwargs["content"] == "Why can't a bicycle stand up by itself? Because it's two-tired!" @@ -665,18 +596,12 @@ def test_anthropic(instrument_legacy, span_exporter, log_exporter): assert output_kwargs["usage_metadata"]["total_tokens"] == 41 logs = log_exporter.get_finished_logs() - assert len(logs) == 0, ( - "Assert that it doesn't emit logs when use_legacy_attributes is True" - ) + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr -def test_anthropic_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant"), ("user", "{input}")] - ) +def test_anthropic_with_events_with_content(instrument_with_content, span_exporter, log_exporter): + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"), ("user", "{input}")]) model = ChatAnthropic(model="claude-2.1", temperature=0.5) chain = prompt | model @@ -698,23 +623,16 @@ def test_anthropic_with_events_with_content( assert anthropic_span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS] == 19 assert anthropic_span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] == 22 assert anthropic_span.attributes[SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS] == 41 - assert ( - anthropic_span.attributes[GenAIAttributes.GEN_AI_RESPONSE_ID] - == "msg_017fMG9SRDFTBhcD1ibtN1nK" - ) + assert anthropic_span.attributes[GenAIAttributes.GEN_AI_RESPONSE_ID] == "msg_017fMG9SRDFTBhcD1ibtN1nK" logs = log_exporter.get_finished_logs() assert len(logs) == 3 # Validate system message Event - assert_message_in_logs( - logs[0], "gen_ai.system.message", {"content": "You are a helpful assistant"} - ) + assert_message_in_logs(logs[0], "gen_ai.system.message", {"content": "You are a helpful assistant"}) # Validate user message Event - assert_message_in_logs( - logs[1], "gen_ai.user.message", {"content": "tell me a short joke"} - ) + assert_message_in_logs(logs[1], "gen_ai.user.message", {"content": "tell me a short joke"}) # Validate AI choice Event choice_event = { @@ -726,12 +644,8 @@ def test_anthropic_with_events_with_content( @pytest.mark.vcr -def test_anthropic_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant"), ("user", "{input}")] - ) +def test_anthropic_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"), ("user", "{input}")]) model = ChatAnthropic(model="claude-2.1", temperature=0.5) chain = prompt | model @@ -753,10 +667,7 @@ def test_anthropic_with_events_with_no_content( assert anthropic_span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS] == 19 assert anthropic_span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] == 22 assert anthropic_span.attributes[SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS] == 41 - assert ( - anthropic_span.attributes[GenAIAttributes.GEN_AI_RESPONSE_ID] - == "msg_017fMG9SRDFTBhcD1ibtN1nK" - ) + assert anthropic_span.attributes[GenAIAttributes.GEN_AI_RESPONSE_ID] == "msg_017fMG9SRDFTBhcD1ibtN1nK" logs = log_exporter.get_finished_logs() assert len(logs) == 3 @@ -778,9 +689,7 @@ def test_anthropic_with_events_with_no_content( @pytest.mark.vcr def test_bedrock(instrument_legacy, span_exporter, log_exporter): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant"), ("user", "{input}")] - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"), ("user", "{input}")]) model = ChatBedrock( model_id="anthropic.claude-3-haiku-20240307-v1:0", client=boto3.client( @@ -804,14 +713,9 @@ def test_bedrock(instrument_legacy, span_exporter, log_exporter): ] == [span.name for span in spans] bedrock_span = next(span for span in spans if span.name == "ChatBedrock.chat") - workflow_span = next( - span for span in spans if span.name == "RunnableSequence.workflow" - ) + workflow_span = next(span for span in spans if span.name == "RunnableSequence.workflow") - assert ( - bedrock_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] - == "anthropic.claude-3-haiku-20240307-v1:0" - ) + assert bedrock_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == "anthropic.claude-3-haiku-20240307-v1:0" assert bedrock_span.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] == "aws.bedrock" system_instructions = json.loads(bedrock_span.attributes[GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS]) assert system_instructions[0]["content"] == "You are a helpful assistant" @@ -821,12 +725,13 @@ def test_bedrock(instrument_legacy, span_exporter, log_exporter): assert bedrock_span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS] == 16 assert bedrock_span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] == 27 assert bedrock_span.attributes[SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS] == 43 - output = json.loads( - workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT] - ) + output = json.loads(workflow_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT]) # We check essential fields instead of exact match due to library version differences output_kwargs = output["outputs"]["kwargs"] - assert output_kwargs["content"] == "Here's a short joke for you:\n\nWhat do you call a bear with no teeth? A gummy bear!" + assert ( + output_kwargs["content"] + == "Here's a short joke for you:\n\nWhat do you call a bear with no teeth? A gummy bear!" + ) assert output_kwargs["type"] == "ai" assert output_kwargs["tool_calls"] == [] assert output_kwargs["invalid_tool_calls"] == [] @@ -837,18 +742,12 @@ def test_bedrock(instrument_legacy, span_exporter, log_exporter): assert output_kwargs["usage_metadata"]["total_tokens"] == 43 logs = log_exporter.get_finished_logs() - assert len(logs) == 0, ( - "Assert that it doesn't emit logs when use_legacy_attributes is True" - ) + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr -def test_bedrock_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant"), ("user", "{input}")] - ) +def test_bedrock_with_events_with_content(instrument_with_content, span_exporter, log_exporter): + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"), ("user", "{input}")]) model = ChatBedrock( model_id="anthropic.claude-3-haiku-20240307-v1:0", client=boto3.client( @@ -873,10 +772,7 @@ def test_bedrock_with_events_with_content( bedrock_span = next(span for span in spans if span.name == "ChatBedrock.chat") - assert ( - bedrock_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] - == "anthropic.claude-3-haiku-20240307-v1:0" - ) + assert bedrock_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == "anthropic.claude-3-haiku-20240307-v1:0" assert bedrock_span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS] == 16 assert bedrock_span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] == 27 @@ -886,14 +782,10 @@ def test_bedrock_with_events_with_content( assert len(logs) == 3 # Validate system message Event - assert_message_in_logs( - logs[0], "gen_ai.system.message", {"content": "You are a helpful assistant"} - ) + assert_message_in_logs(logs[0], "gen_ai.system.message", {"content": "You are a helpful assistant"}) # Validate user message Event - assert_message_in_logs( - logs[1], "gen_ai.user.message", {"content": "tell me a short joke"} - ) + assert_message_in_logs(logs[1], "gen_ai.user.message", {"content": "tell me a short joke"}) # Validate AI choice Event choice_event = { @@ -905,12 +797,8 @@ def test_bedrock_with_events_with_content( @pytest.mark.vcr -def test_bedrock_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant"), ("user", "{input}")] - ) +def test_bedrock_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"), ("user", "{input}")]) model = ChatBedrock( model_id="anthropic.claude-3-haiku-20240307-v1:0", client=boto3.client( @@ -935,10 +823,7 @@ def test_bedrock_with_events_with_no_content( bedrock_span = next(span for span in spans if span.name == "ChatBedrock.chat") - assert ( - bedrock_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] - == "anthropic.claude-3-haiku-20240307-v1:0" - ) + assert bedrock_span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == "anthropic.claude-3-haiku-20240307-v1:0" assert bedrock_span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS] == 16 assert bedrock_span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] == 27 assert bedrock_span.attributes[SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS] == 43 @@ -986,12 +871,8 @@ def assert_request_contains_tracecontext(request: httpx.Request, expected_span: @pytest.mark.vcr @pytest.mark.parametrize("LLM", [OpenAI, VLLMOpenAI, ChatOpenAI]) def test_trace_propagation(instrument_legacy, span_exporter, log_exporter, LLM): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant "), ("human", "{input}")] - ) - model = LLM( - model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20 - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant "), ("human", "{input}")]) + model = LLM(model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20) chain = prompt | model send_spy = spy_decorator(httpx.Client.send) @@ -1002,11 +883,7 @@ def test_trace_propagation(instrument_legacy, span_exporter, log_exporter, LLM): spans = span_exporter.get_finished_spans() openai_span = next(span for span in spans if "OpenAI" in span.name) - expected_vendors = { - OpenAI: "openai", - VLLMOpenAI: "openai", - ChatOpenAI: "openai" - } + expected_vendors = {OpenAI: "openai", VLLMOpenAI: "openai", ChatOpenAI: "openai"} assert openai_span.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] == expected_vendors[LLM] args, kwargs = send_spy.mock.call_args @@ -1015,22 +892,14 @@ def test_trace_propagation(instrument_legacy, span_exporter, log_exporter, LLM): assert_request_contains_tracecontext(request, openai_span) logs = log_exporter.get_finished_logs() - assert len(logs) == 0, ( - "Assert that it doesn't emit logs when use_legacy_attributes is True" - ) + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr @pytest.mark.parametrize("LLM", [OpenAI, VLLMOpenAI, ChatOpenAI]) -def test_trace_propagation_with_events_with_content( - instrument_with_content, span_exporter, log_exporter, LLM -): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant "), ("human", "{input}")] - ) - model = LLM( - model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20 - ) +def test_trace_propagation_with_events_with_content(instrument_with_content, span_exporter, log_exporter, LLM): + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant "), ("human", "{input}")]) + model = LLM(model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20) chain = prompt | model send_spy = spy_decorator(httpx.Client.send) @@ -1099,15 +968,9 @@ def test_trace_propagation_with_events_with_content( @pytest.mark.vcr @pytest.mark.parametrize("LLM", [OpenAI, VLLMOpenAI, ChatOpenAI]) -def test_trace_propagation_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter, LLM -): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant "), ("human", "{input}")] - ) - model = LLM( - model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20 - ) +def test_trace_propagation_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter, LLM): + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant "), ("human", "{input}")]) + model = LLM(model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20) chain = prompt | model send_spy = spy_decorator(httpx.Client.send) @@ -1160,16 +1023,10 @@ def test_trace_propagation_with_events_with_no_content( @pytest.mark.vcr -@pytest.mark.parametrize( - "LLM", [OpenAI, VLLMOpenAI, pytest.param(ChatOpenAI, marks=pytest.mark.xfail)] -) +@pytest.mark.parametrize("LLM", [OpenAI, VLLMOpenAI, pytest.param(ChatOpenAI, marks=pytest.mark.xfail)]) def test_trace_propagation_stream(instrument_legacy, span_exporter, log_exporter, LLM): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant "), ("human", "{input}")] - ) - model = LLM( - model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20 - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant "), ("human", "{input}")]) + model = LLM(model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20) chain = prompt | model send_spy = spy_decorator(httpx.Client.send) @@ -1188,24 +1045,14 @@ def test_trace_propagation_stream(instrument_legacy, span_exporter, log_exporter assert_request_contains_tracecontext(request, openai_span) logs = log_exporter.get_finished_logs() - assert len(logs) == 0, ( - "Assert that it doesn't emit logs when use_legacy_attributes is True" - ) + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr -@pytest.mark.parametrize( - "LLM", [OpenAI, VLLMOpenAI, pytest.param(ChatOpenAI, marks=pytest.mark.xfail)] -) -def test_trace_propagation_stream_with_events_with_content( - instrument_with_content, span_exporter, log_exporter, LLM -): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant "), ("human", "{input}")] - ) - model = LLM( - model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20 - ) +@pytest.mark.parametrize("LLM", [OpenAI, VLLMOpenAI, pytest.param(ChatOpenAI, marks=pytest.mark.xfail)]) +def test_trace_propagation_stream_with_events_with_content(instrument_with_content, span_exporter, log_exporter, LLM): + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant "), ("human", "{input}")]) + model = LLM(model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20) chain = prompt | model send_spy = spy_decorator(httpx.Client.send) @@ -1248,18 +1095,12 @@ def test_trace_propagation_stream_with_events_with_content( @pytest.mark.vcr -@pytest.mark.parametrize( - "LLM", [OpenAI, VLLMOpenAI, pytest.param(ChatOpenAI, marks=pytest.mark.xfail)] -) +@pytest.mark.parametrize("LLM", [OpenAI, VLLMOpenAI, pytest.param(ChatOpenAI, marks=pytest.mark.xfail)]) def test_trace_propagation_stream_with_events_with_no_content( instrument_with_no_content, span_exporter, log_exporter, LLM ): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant "), ("human", "{input}")] - ) - model = LLM( - model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20 - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant "), ("human", "{input}")]) + model = LLM(model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20) chain = prompt | model send_spy = spy_decorator(httpx.Client.send) @@ -1299,15 +1140,9 @@ def test_trace_propagation_stream_with_events_with_no_content( @pytest.mark.asyncio @pytest.mark.vcr @pytest.mark.parametrize("LLM", [OpenAI, VLLMOpenAI, ChatOpenAI]) -async def test_trace_propagation_async( - instrument_legacy, span_exporter, log_exporter, LLM -): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant "), ("human", "{input}")] - ) - model = LLM( - model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20 - ) +async def test_trace_propagation_async(instrument_legacy, span_exporter, log_exporter, LLM): + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant "), ("human", "{input}")]) + model = LLM(model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20) chain = prompt | model send_spy = spy_decorator(httpx.AsyncClient.send) @@ -1324,9 +1159,7 @@ async def test_trace_propagation_async( assert_request_contains_tracecontext(request, openai_span) logs = log_exporter.get_finished_logs() - assert len(logs) == 0, ( - "Assert that it doesn't emit logs when use_legacy_attributes is True" - ) + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.asyncio @@ -1335,12 +1168,8 @@ async def test_trace_propagation_async( async def test_trace_propagation_async_with_events_with_content( instrument_with_content, span_exporter, log_exporter, LLM ): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant "), ("human", "{input}")] - ) - model = LLM( - model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20 - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant "), ("human", "{input}")]) + model = LLM(model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20) chain = prompt | model send_spy = spy_decorator(httpx.AsyncClient.send) @@ -1414,12 +1243,8 @@ async def test_trace_propagation_async_with_events_with_content( async def test_trace_propagation_async_with_events_with_no_content( instrument_with_no_content, span_exporter, log_exporter, LLM ): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant "), ("human", "{input}")] - ) - model = LLM( - model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20 - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant "), ("human", "{input}")]) + model = LLM(model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20) chain = prompt | model send_spy = spy_decorator(httpx.AsyncClient.send) @@ -1473,18 +1298,10 @@ async def test_trace_propagation_async_with_events_with_no_content( @pytest.mark.asyncio @pytest.mark.vcr -@pytest.mark.parametrize( - "LLM", [OpenAI, VLLMOpenAI, pytest.param(ChatOpenAI, marks=pytest.mark.xfail)] -) -async def test_trace_propagation_stream_async( - instrument_legacy, span_exporter, log_exporter, LLM -): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant "), ("human", "{input}")] - ) - model = LLM( - model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20 - ) +@pytest.mark.parametrize("LLM", [OpenAI, VLLMOpenAI, pytest.param(ChatOpenAI, marks=pytest.mark.xfail)]) +async def test_trace_propagation_stream_async(instrument_legacy, span_exporter, log_exporter, LLM): + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant "), ("human", "{input}")]) + model = LLM(model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20) chain = prompt | model send_spy = spy_decorator(httpx.AsyncClient.send) @@ -1503,25 +1320,17 @@ async def test_trace_propagation_stream_async( assert_request_contains_tracecontext(request, openai_span) logs = log_exporter.get_finished_logs() - assert len(logs) == 0, ( - "Assert that it doesn't emit logs when use_legacy_attributes is True" - ) + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.asyncio @pytest.mark.vcr -@pytest.mark.parametrize( - "LLM", [OpenAI, VLLMOpenAI, pytest.param(ChatOpenAI, marks=pytest.mark.xfail)] -) +@pytest.mark.parametrize("LLM", [OpenAI, VLLMOpenAI, pytest.param(ChatOpenAI, marks=pytest.mark.xfail)]) async def test_trace_propagation_stream_async_with_events_with_content( instrument_with_content, span_exporter, log_exporter, LLM ): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant "), ("human", "{input}")] - ) - model = LLM( - model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20 - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant "), ("human", "{input}")]) + model = LLM(model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20) chain = prompt | model send_spy = spy_decorator(httpx.AsyncClient.send) @@ -1565,18 +1374,12 @@ async def test_trace_propagation_stream_async_with_events_with_content( @pytest.mark.asyncio @pytest.mark.vcr -@pytest.mark.parametrize( - "LLM", [OpenAI, VLLMOpenAI, pytest.param(ChatOpenAI, marks=pytest.mark.xfail)] -) +@pytest.mark.parametrize("LLM", [OpenAI, VLLMOpenAI, pytest.param(ChatOpenAI, marks=pytest.mark.xfail)]) async def test_trace_propagation_stream_async_with_events_with_no_content( instrument_with_no_content, span_exporter, log_exporter, LLM ): - prompt = ChatPromptTemplate.from_messages( - [("system", "You are a helpful assistant "), ("human", "{input}")] - ) - model = LLM( - model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20 - ) + prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant "), ("human", "{input}")]) + model = LLM(model="facebook/opt-125m", base_url="http://localhost:8000/v1", max_tokens=20) chain = prompt | model send_spy = spy_decorator(httpx.AsyncClient.send) diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_non_ascii_content.py b/packages/opentelemetry-instrumentation-langchain/tests/test_non_ascii_content.py index 6435879892..e841769e5f 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/test_non_ascii_content.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_non_ascii_content.py @@ -37,9 +37,7 @@ def test_chain_start_preserves_non_ascii_in_entity_input(callback_handler, span_ text = "こんにちは世界" _start_chain(callback_handler, run_id, inputs={"query": text}) - attr = callback_handler.spans[run_id].span.attributes.get( - SpanAttributes.TRACELOOP_ENTITY_INPUT - ) + attr = callback_handler.spans[run_id].span.attributes.get(SpanAttributes.TRACELOOP_ENTITY_INPUT) assert text in attr assert "\\u3053" not in attr assert json.loads(attr)["inputs"]["query"] == text @@ -89,9 +87,7 @@ def test_tool_start_preserves_non_ascii_in_entity_input(callback_handler, span_e inputs={"text": text}, ) - attr = callback_handler.spans[run_id].span.attributes.get( - SpanAttributes.TRACELOOP_ENTITY_INPUT - ) + attr = callback_handler.spans[run_id].span.attributes.get(SpanAttributes.TRACELOOP_ENTITY_INPUT) assert text in attr assert "\\u00c4" not in attr assert json.loads(attr)["input_str"] == text diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_structured_output.py b/packages/opentelemetry-instrumentation-langchain/tests/test_structured_output.py index a18ef055ed..54875519b0 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/test_structured_output.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_structured_output.py @@ -37,15 +37,11 @@ def test_structured_output(instrument_legacy, span_exporter, log_exporter): assert input_messages[0]["parts"][0]["content"] == query_text logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.vcr -def test_structured_output_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +def test_structured_output_with_events_with_content(instrument_with_content, span_exporter, log_exporter): query_text = "Analyze the following food item: avocado" query = [HumanMessage(content=query_text)] model = ChatOpenAI(model="gpt-4o-mini", temperature=0) @@ -75,9 +71,7 @@ def test_structured_output_with_events_with_content( @pytest.mark.vcr -def test_structured_output_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +def test_structured_output_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): query_text = "Analyze the following food item: avocado" query = [HumanMessage(content=query_text)] model = ChatOpenAI(model="gpt-4o-mini", temperature=0) diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_tool_call_content.py b/packages/opentelemetry-instrumentation-langchain/tests/test_tool_call_content.py index d90dc8e245..232ec831fd 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/test_tool_call_content.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_tool_call_content.py @@ -57,18 +57,12 @@ def test_assistant_message_with_tool_calls_includes_content(): # Message 0: user message assert input_messages[0]["role"] == "user" assert input_messages[0]["parts"][0]["type"] == "text" - assert ( - input_messages[0]["parts"][0]["content"] - == "what is the current time? First greet me." - ) + assert input_messages[0]["parts"][0]["content"] == "what is the current time? First greet me." # Message 1: assistant message with content and tool_calls assert input_messages[1]["role"] == "assistant" assert input_messages[1]["parts"][0]["type"] == "text" - assert ( - input_messages[1]["parts"][0]["content"] - == "Hello! Let me check the current time for you." - ) + assert input_messages[1]["parts"][0]["content"] == "Hello! Let me check the current time for you." assert input_messages[1]["parts"][1]["type"] == "tool_call" assert input_messages[1]["parts"][1]["id"] == "call_qU7pH3EdQvzwkPyKPOdpgaKA" assert input_messages[1]["parts"][1]["name"] == "get_current_time" @@ -82,10 +76,7 @@ def test_assistant_message_with_tool_calls_includes_content(): # Message 3: assistant message with only content assert input_messages[3]["role"] == "assistant" assert input_messages[3]["parts"][0]["type"] == "text" - assert ( - input_messages[3]["parts"][0]["content"] - == "The current time is 2025-08-15 08:15:21" - ) + assert input_messages[3]["parts"][0]["content"] == "The current time is 2025-08-15 08:15:21" def test_assistant_message_with_only_tool_calls_no_content(): @@ -102,9 +93,7 @@ def test_assistant_message_with_only_tool_calls_no_content(): [ AIMessage( content="", - tool_calls=[ - {"id": "call_123", "name": "some_tool", "args": {"param": "value"}} - ], + tool_calls=[{"id": "call_123", "name": "some_tool", "args": {"param": "value"}}], ) ] ] @@ -148,10 +137,7 @@ def test_assistant_message_with_only_content_no_tool_calls(): assert input_messages[0]["role"] == "assistant" assert input_messages[0]["parts"][0]["type"] == "text" - assert ( - input_messages[0]["parts"][0]["content"] - == "Just a regular response with no tool calls" - ) + assert input_messages[0]["parts"][0]["content"] == "Just a regular response with no tool calls" tool_call_parts = [p for p in input_messages[0]["parts"] if p["type"] == "tool_call"] assert len(tool_call_parts) == 0 diff --git a/packages/opentelemetry-instrumentation-langchain/tests/test_tool_calls.py b/packages/opentelemetry-instrumentation-langchain/tests/test_tool_calls.py index f1ad7f9579..7c16c0e605 100644 --- a/packages/opentelemetry-instrumentation-langchain/tests/test_tool_calls.py +++ b/packages/opentelemetry-instrumentation-langchain/tests/test_tool_calls.py @@ -17,9 +17,7 @@ ) -def food_analysis( - name: str, healthy: bool, calories: int, taste_profile: List[str] -) -> str: +def food_analysis(name: str, healthy: bool, calories: int, taste_profile: List[str]) -> str: return "pass" @@ -70,16 +68,12 @@ def test_tool_calls(instrument_legacy, span_exporter, log_exporter): # ) logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.skip(reason="Direct model invocations do not create langchain spans") @pytest.mark.vcr -def test_tool_calls_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +def test_tool_calls_with_events_with_content(instrument_with_content, span_exporter, log_exporter): query_text = "Analyze the following food item: avocado" query = [HumanMessage(content=query_text)] model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) @@ -123,9 +117,7 @@ def test_tool_calls_with_events_with_content( @pytest.mark.skip(reason="Direct model invocations do not create langchain spans") @pytest.mark.vcr -def test_tool_calls_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +def test_tool_calls_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): query_text = "Analyze the following food item: avocado" query = [HumanMessage(content=query_text)] model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) @@ -228,22 +220,16 @@ def get_weather(location: str) -> str: assert output_messages[0]["parts"][0]["type"] == "tool_call" assert output_messages[0]["parts"][0]["name"] == "get_weather" - result_arguments = result.model_dump()["additional_kwargs"]["tool_calls"][0][ - "function" - ]["arguments"] + result_arguments = result.model_dump()["additional_kwargs"]["tool_calls"][0]["function"]["arguments"] assert output_messages[0]["parts"][0]["arguments"] == json.loads(result_arguments) logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.skip(reason="Direct model invocations do not create langchain spans") @pytest.mark.vcr -def test_tool_calls_with_history_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +def test_tool_calls_with_history_with_events_with_content(instrument_with_content, span_exporter, log_exporter): def get_weather(location: str) -> str: return "sunny" @@ -277,9 +263,7 @@ def get_weather(location: str) -> str: assert len(logs) == 6 # Validate system message Event - assert_message_in_logs( - logs[0], "gen_ai.system.message", {"content": "Be crisp and friendly."} - ) + assert_message_in_logs(logs[0], "gen_ai.system.message", {"content": "Be crisp and friendly."}) # Validate user message Event assert_message_in_logs( @@ -308,14 +292,10 @@ def get_weather(location: str) -> str: ) # Validate tool message Event - assert_message_in_logs( - logs[3], "gen_ai.tool.message", {"content": "Sunny as always!"} - ) + assert_message_in_logs(logs[3], "gen_ai.tool.message", {"content": "Sunny as always!"}) # Validate second user message Event - assert_message_in_logs( - logs[4], "gen_ai.user.message", {"content": "What's the weather in London?"} - ) + assert_message_in_logs(logs[4], "gen_ai.user.message", {"content": "What's the weather in London?"}) # Validate AI choice Event tool_call = result.model_dump()["additional_kwargs"]["tool_calls"][0] @@ -339,9 +319,7 @@ def get_weather(location: str) -> str: @pytest.mark.skip(reason="Direct model invocations do not create langchain spans") @pytest.mark.vcr -def test_tool_calls_with_history_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +def test_tool_calls_with_history_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): def get_weather(location: str) -> str: return "sunny" @@ -420,9 +398,7 @@ def get_weather(location: str) -> str: @pytest.mark.skip(reason="Direct model invocations do not create langchain spans") @pytest.mark.vcr -def test_tool_calls_anthropic_text_block( - instrument_legacy, span_exporter, log_exporter -): +def test_tool_calls_anthropic_text_block(instrument_legacy, span_exporter, log_exporter): # This test checks for cases when anthropic prepends a tool call with a text block. def get_weather(location: str) -> str: @@ -432,9 +408,7 @@ def get_news(location: str) -> str: return "Not much" messages: list[BaseMessage] = [ - HumanMessage( - content="Hey, what's the weather in San Francisco? Also, any news in town?" - ), + HumanMessage(content="Hey, what's the weather in San Francisco? Also, any news in town?"), ] model = ChatAnthropic(model="claude-3-5-haiku-latest") model_with_tools = model.bind_tools([get_weather, get_news]) @@ -479,16 +453,12 @@ def get_news(location: str) -> str: assert output_messages[0]["parts"][1]["arguments"] == {"location": "San Francisco"} logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.skip(reason="Direct model invocations do not create langchain spans") @pytest.mark.vcr -def test_tool_calls_anthropic_text_block_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +def test_tool_calls_anthropic_text_block_with_events_with_content(instrument_with_content, span_exporter, log_exporter): # This test checks for cases when anthropic prepends a tool call with a text block. def get_weather(location: str) -> str: @@ -498,9 +468,7 @@ def get_news(location: str) -> str: return "Not much" messages: list[BaseMessage] = [ - HumanMessage( - content="Hey, what's the weather in San Francisco? Also, any news in town?" - ), + HumanMessage(content="Hey, what's the weather in San Francisco? Also, any news in town?"), ] model = ChatAnthropic(model="claude-3-5-haiku-latest") model_with_tools = model.bind_tools([get_weather, get_news]) @@ -518,9 +486,7 @@ def get_news(location: str) -> str: assert_message_in_logs( logs[0], "gen_ai.user.message", - { - "content": "Hey, what's the weather in San Francisco? Also, any news in town?" - }, + {"content": "Hey, what's the weather in San Francisco? Also, any news in town?"}, ) # Validate AI choice Event @@ -557,9 +523,7 @@ def get_news(location: str) -> str: return "Not much" messages: list[BaseMessage] = [ - HumanMessage( - content="Hey, what's the weather in San Francisco? Also, any news in town?" - ), + HumanMessage(content="Hey, what's the weather in San Francisco? Also, any news in town?"), ] model = ChatAnthropic(model="claude-3-5-haiku-latest") model_with_tools = model.bind_tools([get_weather, get_news]) @@ -595,9 +559,7 @@ def get_news(location: str) -> str: @pytest.mark.skip(reason="Direct model invocations do not create langchain spans") @pytest.mark.vcr -def test_tool_calls_anthropic_text_block_and_history( - instrument_legacy, span_exporter, log_exporter -): +def test_tool_calls_anthropic_text_block_and_history(instrument_legacy, span_exporter, log_exporter): # This test checks for cases when anthropic prepends a tool call with a text block # and then the response messaged is added to the history. def get_weather(location: str) -> str: @@ -607,9 +569,7 @@ def get_news(location: str) -> str: return "Not much" messages: list[BaseMessage] = [ - HumanMessage( - content="Hey, what's the weather in San Francisco? Also, any news in town?" - ), + HumanMessage(content="Hey, what's the weather in San Francisco? Also, any news in town?"), AIMessage( content=[ { @@ -633,9 +593,7 @@ def get_news(location: str) -> str: } ], ), - ToolMessage( - content="Sunny as always!", tool_call_id="toolu_016q9vtSd8CY2vnZSpEp1j4o" - ), + ToolMessage(content="Sunny as always!", tool_call_id="toolu_016q9vtSd8CY2vnZSpEp1j4o"), ] model = ChatAnthropic(model="claude-3-5-haiku-latest") model_with_tools = model.bind_tools([get_weather, get_news]) @@ -691,9 +649,7 @@ def get_news(location: str) -> str: assert output_messages[0]["parts"][1]["arguments"] == {"location": "San Francisco"} logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.skip(reason="Direct model invocations do not create langchain spans") @@ -710,9 +666,7 @@ def get_news(location: str) -> str: return "Not much" messages: list[BaseMessage] = [ - HumanMessage( - content="Hey, what's the weather in San Francisco? Also, any news in town?" - ), + HumanMessage(content="Hey, what's the weather in San Francisco? Also, any news in town?"), AIMessage( content=[ { @@ -736,9 +690,7 @@ def get_news(location: str) -> str: } ], ), - ToolMessage( - content="Sunny as always!", tool_call_id="toolu_016q9vtSd8CY2vnZSpEp1j4o" - ), + ToolMessage(content="Sunny as always!", tool_call_id="toolu_016q9vtSd8CY2vnZSpEp1j4o"), ] model = ChatAnthropic(model="claude-3-5-haiku-latest") model_with_tools = model.bind_tools([get_weather, get_news]) @@ -754,9 +706,7 @@ def get_news(location: str) -> str: assert len(logs) == 4 # Validate user message Event - assert_message_in_logs( - logs[0], "gen_ai.user.message", {"content": messages[0].content} - ) + assert_message_in_logs(logs[0], "gen_ai.user.message", {"content": messages[0].content}) # Validate AI message Event assert_message_in_logs( @@ -778,9 +728,7 @@ def get_news(location: str) -> str: ) # Validate tool message Event - assert_message_in_logs( - logs[2], "gen_ai.tool.message", {"content": messages[2].content} - ) + assert_message_in_logs(logs[2], "gen_ai.tool.message", {"content": messages[2].content}) # Validate AI choice Event result_dict = result.model_dump() @@ -806,6 +754,7 @@ def get_news(location: str) -> str: @pytest.mark.vcr def test_tool_message_with_tool_call_id(instrument_legacy, span_exporter, log_exporter): """Test that tool_call_id is properly set in span attributes for ToolMessage.""" + def sample_tool(query: str) -> str: return "Tool response" @@ -842,9 +791,7 @@ def sample_tool(query: str) -> str: assert input_messages[2]["parts"][0]["id"] == "call_12345" logs = log_exporter.get_finished_logs() - assert len(logs) == 0, ( - "Assert that it doesn't emit logs when use_legacy_attributes is True" - ) + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.skip(reason="Direct model invocations do not create langchain spans") @@ -861,9 +808,7 @@ def get_news(location: str) -> str: return "Not much" messages: list[BaseMessage] = [ - HumanMessage( - content="Hey, what's the weather in San Francisco? Also, any news in town?" - ), + HumanMessage(content="Hey, what's the weather in San Francisco? Also, any news in town?"), AIMessage( content=[ { @@ -887,9 +832,7 @@ def get_news(location: str) -> str: } ], ), - ToolMessage( - content="Sunny as always!", tool_call_id="toolu_016q9vtSd8CY2vnZSpEp1j4o" - ), + ToolMessage(content="Sunny as always!", tool_call_id="toolu_016q9vtSd8CY2vnZSpEp1j4o"), ] model = ChatAnthropic(model="claude-3-5-haiku-latest") model_with_tools = model.bind_tools([get_weather, get_news]) @@ -952,9 +895,7 @@ def get_news(location: str) -> str: return "Not much" messages: list[BaseMessage] = [ - HumanMessage( - content="Hey, what's the weather in San Francisco? Also, any news in town?" - ), + HumanMessage(content="Hey, what's the weather in San Francisco? Also, any news in town?"), ] model = ChatOpenAI(model="gpt-4.1-nano") model_with_tools = model.bind_tools([get_weather, get_news]) @@ -1001,16 +942,12 @@ def get_news(location: str) -> str: assert output_messages[0]["parts"][1]["arguments"] == {"location": "San Francisco"} logs = log_exporter.get_finished_logs() - assert ( - len(logs) == 0 - ), "Assert that it doesn't emit logs when use_legacy_attributes is True" + assert len(logs) == 0, "Assert that it doesn't emit logs when use_legacy_attributes is True" @pytest.mark.skip(reason="Direct model invocations do not create langchain spans") @pytest.mark.vcr -def test_parallel_tool_calls_with_events_with_content( - instrument_with_content, span_exporter, log_exporter -): +def test_parallel_tool_calls_with_events_with_content(instrument_with_content, span_exporter, log_exporter): def get_weather(location: str) -> str: return "sunny" @@ -1018,9 +955,7 @@ def get_news(location: str) -> str: return "Not much" messages: list[BaseMessage] = [ - HumanMessage( - content="Hey, what's the weather in San Francisco? Also, any news in town?" - ), + HumanMessage(content="Hey, what's the weather in San Francisco? Also, any news in town?"), ] model = ChatOpenAI(model="gpt-4.1-nano") model_with_tools = model.bind_tools([get_weather, get_news]) @@ -1035,9 +970,7 @@ def get_news(location: str) -> str: assert len(logs) == 2 # Validate user message Event - assert_message_in_logs( - logs[0], "gen_ai.user.message", {"content": messages[0].content} - ) + assert_message_in_logs(logs[0], "gen_ai.user.message", {"content": messages[0].content}) # Validate AI choice Event result_dict = result.model_dump() @@ -1070,9 +1003,7 @@ def get_news(location: str) -> str: @pytest.mark.skip(reason="Direct model invocations do not create langchain spans") @pytest.mark.vcr -def test_parallel_tool_calls_with_events_with_no_content( - instrument_with_no_content, span_exporter, log_exporter -): +def test_parallel_tool_calls_with_events_with_no_content(instrument_with_no_content, span_exporter, log_exporter): def get_weather(location: str) -> str: return "sunny" @@ -1080,9 +1011,7 @@ def get_news(location: str) -> str: return "Not much" messages: list[BaseMessage] = [ - HumanMessage( - content="Hey, what's the weather in San Francisco? Also, any news in town?" - ), + HumanMessage(content="Hey, what's the weather in San Francisco? Also, any news in town?"), ] model = ChatOpenAI(model="gpt-4.1-nano") model_with_tools = model.bind_tools([get_weather, get_news]) diff --git a/packages/opentelemetry-instrumentation-langchain/uv.lock b/packages/opentelemetry-instrumentation-langchain/uv.lock index c5d6b22d38..66db0c4d55 100644 --- a/packages/opentelemetry-instrumentation-langchain/uv.lock +++ b/packages/opentelemetry-instrumentation-langchain/uv.lock @@ -420,7 +420,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1479,7 +1479,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-bedrock" -version = "0.61.0" +version = "0.62.1" source = { editable = "../opentelemetry-instrumentation-bedrock" } dependencies = [ { name = "opentelemetry-api" }, @@ -1515,7 +1515,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-langchain" -version = "0.61.0" +version = "0.62.1" source = { editable = "." } dependencies = [ { name = "opentelemetry-api" }, @@ -1607,7 +1607,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-openai" -version = "0.61.0" +version = "0.62.1" source = { editable = "../opentelemetry-instrumentation-openai" } dependencies = [ { name = "opentelemetry-api" },