diff --git a/python/.env.example b/python/.env.example index f864f18f72..c09300d775 100644 --- a/python/.env.example +++ b/python/.env.example @@ -33,7 +33,6 @@ ANTHROPIC_MODEL="" OLLAMA_ENDPOINT="" OLLAMA_MODEL="" # Observability -ENABLE_OTEL=true +ENABLE_INSTRUMENTATION=true ENABLE_SENSITIVE_DATA=true -OTLP_ENDPOINT="http://localhost:4317/" -# APPLICATIONINSIGHTS_CONNECTION_STRING="..." +OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317/" diff --git a/python/packages/a2a/agent_framework_a2a/_agent.py b/python/packages/a2a/agent_framework_a2a/_agent.py index 4f86eb5afc..f90d7214a0 100644 --- a/python/packages/a2a/agent_framework_a2a/_agent.py +++ b/python/packages/a2a/agent_framework_a2a/_agent.py @@ -5,7 +5,7 @@ import re import uuid from collections.abc import AsyncIterable, Sequence -from typing import Any, cast +from typing import Any, Final, cast import httpx from a2a.client import Client, ClientConfig, ClientFactory, minimal_agent_card @@ -38,6 +38,7 @@ UriContent, prepend_agent_framework_to_user_agent, ) +from agent_framework.observability import use_agent_instrumentation __all__ = ["A2AAgent"] @@ -58,6 +59,7 @@ def _get_uri_data(uri: str) -> str: return match.group("base64_data") +@use_agent_instrumentation class A2AAgent(BaseAgent): """Agent2Agent (A2A) protocol implementation. @@ -69,6 +71,8 @@ class A2AAgent(BaseAgent): Can be initialized with a URL, AgentCard, or existing A2A Client instance. """ + AGENT_PROVIDER_NAME: Final[str] = "A2A" + def __init__( self, *, diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_client.py b/python/packages/ag-ui/agent_framework_ag_ui/_client.py index ab7eb53940..db2f160a9d 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_client.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_client.py @@ -23,7 +23,7 @@ from agent_framework._middleware import use_chat_middleware from agent_framework._tools import use_function_invocation from agent_framework._types import BaseContent, Contents -from agent_framework.observability import use_observability +from agent_framework.observability import use_instrumentation from ._event_converters import AGUIEventConverter from ._http_service import AGUIHttpService @@ -89,7 +89,7 @@ async def response_wrapper(self, *args: Any, **kwargs: Any) -> ChatResponse: @_apply_server_function_call_unwrap @use_function_invocation -@use_observability +@use_instrumentation @use_chat_middleware class AGUIChatClient(BaseChatClient): """Chat client for communicating with AG-UI compliant servers. diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 96a70bc4a0..e4eca2d005 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -35,7 +35,7 @@ ) from agent_framework._pydantic import AFBaseSettings from agent_framework.exceptions import ServiceInitializationError -from agent_framework.observability import use_observability +from agent_framework.observability import use_instrumentation from anthropic import AsyncAnthropic from anthropic.types.beta import ( BetaContentBlock, @@ -110,7 +110,7 @@ class AnthropicSettings(AFBaseSettings): @use_function_invocation -@use_observability +@use_instrumentation @use_chat_middleware class AnthropicClient(BaseChatClient): """Anthropic Chat client.""" diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py index 0ea9ee1f05..839687fbaf 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py @@ -43,7 +43,7 @@ use_function_invocation, ) from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException -from agent_framework.observability import use_observability +from agent_framework.observability import use_instrumentation from azure.ai.agents.aio import AgentsClient from azure.ai.agents.models import ( Agent, @@ -107,7 +107,7 @@ @use_function_invocation -@use_observability +@use_instrumentation @use_chat_middleware class AzureAIAgentClient(BaseChatClient): """Azure AI Agent Chat client.""" diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index 63bd2b27df..a11981390c 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -15,7 +15,7 @@ use_function_invocation, ) from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError -from agent_framework.observability import use_observability +from agent_framework.observability import use_instrumentation from agent_framework.openai._responses_client import OpenAIBaseResponsesClient from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( @@ -49,7 +49,7 @@ @use_function_invocation -@use_observability +@use_instrumentation @use_chat_middleware class AzureAIClient(OpenAIBaseResponsesClient): """Azure AI Agent client.""" @@ -164,27 +164,94 @@ def __init__( # Track whether we should close client connection self._should_close_client = should_close_client - async def setup_azure_ai_observability(self, enable_sensitive_data: bool | None = None) -> None: - """Use this method to setup tracing in your Azure AI Project. + async def configure_azure_monitor( + self, + enable_sensitive_data: bool = False, + **kwargs: Any, + ) -> None: + """Setup observability with Azure Monitor (Azure AI Foundry integration). + + This method configures Azure Monitor for telemetry collection using the + connection string from the Azure AI project client. - This will take the connection string from the project project_client. - It will override any connection string that is set in the environment variables. - It will disable any OTLP endpoint that might have been set. + Args: + enable_sensitive_data: Enable sensitive data logging (prompts, responses). + Should only be enabled in development/test environments. Default is False. + **kwargs: Additional arguments passed to configure_azure_monitor(). + Common options include: + - enable_live_metrics (bool): Enable Azure Monitor Live Metrics + - credential (TokenCredential): Azure credential for Entra ID auth + - resource (Resource): Custom OpenTelemetry resource + See https://learn.microsoft.com/python/api/azure-monitor-opentelemetry/azure.monitor.opentelemetry.configure_azure_monitor + for full list of options. + + Raises: + ImportError: If azure-monitor-opentelemetry-exporter is not installed. + + Examples: + .. code-block:: python + + from agent_framework.azure import AzureAIClient + from azure.ai.projects.aio import AIProjectClient + from azure.identity.aio import DefaultAzureCredential + + async with ( + DefaultAzureCredential() as credential, + AIProjectClient( + endpoint="https://your-project.api.azureml.ms", credential=credential + ) as project_client, + AzureAIClient(project_client=project_client) as client, + ): + # Setup observability with defaults + await client.configure_azure_monitor() + + # With live metrics enabled + await client.configure_azure_monitor(enable_live_metrics=True) + + # With sensitive data logging (dev/test only) + await client.configure_azure_monitor(enable_sensitive_data=True) + + Note: + This method retrieves the Application Insights connection string from the + Azure AI project client automatically. You must have Application Insights + configured in your Azure AI project for this to work. """ + # Get connection string from project client try: conn_string = await self.project_client.telemetry.get_application_insights_connection_string() except ResourceNotFoundError: logger.warning( - "No Application Insights connection string found for the Azure AI Project, " - "please call setup_observability() manually." + "No Application Insights connection string found for the Azure AI Project. " + "Please ensure Application Insights is configured in your Azure AI project, " + "or call configure_otel_providers() manually with custom exporters." ) return - from agent_framework.observability import setup_observability - setup_observability( - applicationinsights_connection_string=conn_string, enable_sensitive_data=enable_sensitive_data + # Import Azure Monitor with proper error handling + try: + from azure.monitor.opentelemetry import configure_azure_monitor + except ImportError as exc: + raise ImportError( + "azure-monitor-opentelemetry is required for Azure Monitor integration. " + "Install it with: pip install azure-monitor-opentelemetry" + ) from exc + + from agent_framework.observability import create_metric_views, create_resource, enable_instrumentation + + # Create resource if not provided in kwargs + if "resource" not in kwargs: + kwargs["resource"] = create_resource() + + # Configure Azure Monitor with connection string and kwargs + configure_azure_monitor( + connection_string=conn_string, + views=create_metric_views(), + **kwargs, ) + # Complete setup with core observability + enable_instrumentation(enable_sensitive_data=enable_sensitive_data) + async def __aenter__(self) -> "Self": """Async context manager entry.""" return self diff --git a/python/packages/core/agent_framework/_agents.py b/python/packages/core/agent_framework/_agents.py index 3c40004362..aadd1be40a 100644 --- a/python/packages/core/agent_framework/_agents.py +++ b/python/packages/core/agent_framework/_agents.py @@ -34,7 +34,7 @@ ToolMode, ) from .exceptions import AgentExecutionException, AgentInitializationError -from .observability import use_agent_observability +from .observability import use_agent_instrumentation if sys.version_info >= (3, 12): from typing import override # type: ignore # pragma: no cover @@ -516,8 +516,8 @@ def _prepare_context_providers( @use_agent_middleware -@use_agent_observability -class ChatAgent(BaseAgent): +@use_agent_instrumentation(capture_usage=False) # type: ignore[arg-type,misc] +class ChatAgent(BaseAgent): # type: ignore[misc] """A Chat Client Agent. This is the primary agent implementation that uses a chat client to interact @@ -583,7 +583,7 @@ def get_weather(location: str) -> str: print(update.text, end="") """ - AGENT_SYSTEM_NAME: ClassVar[str] = "microsoft.agent_framework" + AGENT_PROVIDER_NAME: ClassVar[str] = "microsoft.agent_framework" def __init__( self, diff --git a/python/packages/core/agent_framework/_clients.py b/python/packages/core/agent_framework/_clients.py index 4d91492822..506a1be7cd 100644 --- a/python/packages/core/agent_framework/_clients.py +++ b/python/packages/core/agent_framework/_clients.py @@ -8,7 +8,6 @@ from pydantic import BaseModel from ._logging import get_logger -from ._mcp import MCPTool from ._memory import AggregateContextProvider, ContextProvider from ._middleware import ( ChatMiddleware, @@ -426,6 +425,8 @@ async def _normalize_tools( else [tools] ) for tool in tools_list: # type: ignore[reportUnknownType] + from ._mcp import MCPTool + if isinstance(tool, MCPTool): if not tool.is_connected: await tool.connect() diff --git a/python/packages/core/agent_framework/_memory.py b/python/packages/core/agent_framework/_memory.py index 4b2a01ad24..a5b53fc39f 100644 --- a/python/packages/core/agent_framework/_memory.py +++ b/python/packages/core/agent_framework/_memory.py @@ -6,11 +6,13 @@ from collections.abc import MutableSequence, Sequence from contextlib import AsyncExitStack from types import TracebackType -from typing import Any, Final, cast +from typing import TYPE_CHECKING, Any, Final, cast -from ._tools import ToolProtocol from ._types import ChatMessage +if TYPE_CHECKING: + from ._tools import ToolProtocol + if sys.version_info >= (3, 12): from typing import override # type: ignore # pragma: no cover else: @@ -54,7 +56,7 @@ def __init__( self, instructions: str | None = None, messages: Sequence[ChatMessage] | None = None, - tools: Sequence[ToolProtocol] | None = None, + tools: Sequence["ToolProtocol"] | None = None, ): """Create a new Context object. @@ -65,7 +67,7 @@ def __init__( """ self.instructions = instructions self.messages: Sequence[ChatMessage] = messages or [] - self.tools: Sequence[ToolProtocol] = tools or [] + self.tools: Sequence["ToolProtocol"] = tools or [] # region ContextProvider @@ -247,7 +249,7 @@ async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], * contexts = await asyncio.gather(*[provider.invoking(messages, **kwargs) for provider in self.providers]) instructions: str = "" return_messages: list[ChatMessage] = [] - tools: list[ToolProtocol] = [] + tools: list["ToolProtocol"] = [] for ctx in contexts: if ctx.instructions: instructions += ctx.instructions diff --git a/python/packages/core/agent_framework/_serialization.py b/python/packages/core/agent_framework/_serialization.py index 1a38d9030a..cf28df2f4f 100644 --- a/python/packages/core/agent_framework/_serialization.py +++ b/python/packages/core/agent_framework/_serialization.py @@ -339,11 +339,17 @@ def to_dict(self, *, exclude: set[str] | None = None, exclude_none: bool = True) continue # Handle dicts containing SerializationProtocol values if isinstance(value, dict): + from datetime import date, datetime, time + serialized_dict: dict[str, Any] = {} for k, v in value.items(): if isinstance(v, SerializationProtocol): serialized_dict[k] = v.to_dict(exclude=exclude, exclude_none=exclude_none) continue + # Convert datetime objects to strings + if isinstance(v, (datetime, date, time)): + serialized_dict[k] = str(v) + continue # Check if the value is JSON serializable if is_serializable(v): serialized_dict[k] = v diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index f4662352a0..26e4358fb1 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -1816,13 +1816,14 @@ def prepare_function_call_results(content: Contents | Any | list[Contents | Any] """Prepare the values of the function call results.""" if isinstance(content, Contents): # For BaseContent objects, use to_dict and serialize to JSON - return json.dumps(content.to_dict(exclude={"raw_representation", "additional_properties"})) + # Use default=str to handle datetime and other non-JSON-serializable objects + return json.dumps(content.to_dict(exclude={"raw_representation", "additional_properties"}), default=str) dumpable = _prepare_function_call_results_as_dumpable(content) if isinstance(dumpable, str): return dumpable - # fallback - return json.dumps(dumpable) + # fallback - use default=str to handle datetime and other non-JSON-serializable objects + return json.dumps(dumpable, default=str) # region Chat Response constants diff --git a/python/packages/core/agent_framework/azure/_chat_client.py b/python/packages/core/agent_framework/azure/_chat_client.py index 544c0fdf5b..a8bfec0427 100644 --- a/python/packages/core/agent_framework/azure/_chat_client.py +++ b/python/packages/core/agent_framework/azure/_chat_client.py @@ -21,7 +21,7 @@ use_function_invocation, ) from agent_framework.exceptions import ServiceInitializationError -from agent_framework.observability import use_observability +from agent_framework.observability import use_instrumentation from agent_framework.openai._chat_client import OpenAIBaseChatClient from ._shared import ( @@ -41,7 +41,7 @@ @use_function_invocation -@use_observability +@use_instrumentation @use_chat_middleware class AzureOpenAIChatClient(AzureOpenAIConfigMixin, OpenAIBaseChatClient): """Azure OpenAI Chat completion class.""" diff --git a/python/packages/core/agent_framework/azure/_responses_client.py b/python/packages/core/agent_framework/azure/_responses_client.py index 1d88d51688..3f6140eeeb 100644 --- a/python/packages/core/agent_framework/azure/_responses_client.py +++ b/python/packages/core/agent_framework/azure/_responses_client.py @@ -10,7 +10,7 @@ from agent_framework import use_chat_middleware, use_function_invocation from agent_framework.exceptions import ServiceInitializationError -from agent_framework.observability import use_observability +from agent_framework.observability import use_instrumentation from agent_framework.openai._responses_client import OpenAIBaseResponsesClient from ._shared import ( @@ -22,7 +22,7 @@ @use_function_invocation -@use_observability +@use_instrumentation @use_chat_middleware class AzureOpenAIResponsesClient(AzureOpenAIConfigMixin, OpenAIBaseResponsesClient): """Azure Responses completion class.""" diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py index f3e1d9bd68..38fca796c1 100644 --- a/python/packages/core/agent_framework/observability.py +++ b/python/packages/core/agent_framework/observability.py @@ -3,15 +3,19 @@ import contextlib import json import logging +import os from collections.abc import AsyncIterable, Awaitable, Callable, Generator, Mapping from enum import Enum from functools import wraps from time import perf_counter, time_ns from typing import TYPE_CHECKING, Any, ClassVar, Final, TypeVar +from dotenv import load_dotenv from opentelemetry import metrics, trace +from opentelemetry.sdk.resources import Resource +from opentelemetry.semconv.attributes import service_attributes from opentelemetry.semconv_ai import GenAISystem, Meters, SpanAttributes -from pydantic import BaseModel, PrivateAttr +from pydantic import PrivateAttr from . import __version__ as version_info from ._logging import get_logger @@ -19,10 +23,9 @@ from .exceptions import AgentInitializationError, ChatClientInitializationError if TYPE_CHECKING: # pragma: no cover - from azure.core.credentials import TokenCredential from opentelemetry.sdk._logs.export import LogRecordExporter from opentelemetry.sdk.metrics.export import MetricExporter - from opentelemetry.sdk.resources import Resource + from opentelemetry.sdk.metrics.view import View from opentelemetry.sdk.trace.export import SpanExporter from opentelemetry.trace import Tracer from opentelemetry.util._decorator import _AgnosticContextManager # type: ignore[reportPrivateUsage] @@ -44,11 +47,14 @@ __all__ = [ "OBSERVABILITY_SETTINGS", "OtelAttr", + "configure_otel_providers", + "create_metric_views", + "create_resource", + "enable_instrumentation", "get_meter", "get_tracer", - "setup_observability", - "use_agent_observability", - "use_observability", + "use_agent_instrumentation", + "use_instrumentation", ] @@ -259,89 +265,293 @@ def __str__(self) -> str: # region Telemetry utils -def _get_otlp_exporters(endpoints: list[str]) -> list["LogRecordExporter | SpanExporter | MetricExporter"]: - """Create standard OTLP Exporters for the supplied endpoints.""" - from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter - from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter - from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +# Parse headers helper +def _parse_headers(header_str: str) -> dict[str, str]: + """Parse header string like 'key1=value1,key2=value2' into dict.""" + headers: dict[str, str] = {} + if not header_str: + return headers + for pair in header_str.split(","): + if "=" in pair: + key, value = pair.split("=", 1) + headers[key.strip()] = value.strip() + return headers + + +def _create_otlp_exporters( + endpoint: str | None = None, + protocol: str = "grpc", + headers: dict[str, str] | None = None, + traces_endpoint: str | None = None, + traces_headers: dict[str, str] | None = None, + metrics_endpoint: str | None = None, + metrics_headers: dict[str, str] | None = None, + logs_endpoint: str | None = None, + logs_headers: dict[str, str] | None = None, +) -> list["LogRecordExporter | SpanExporter | MetricExporter"]: + """Create OTLP exporters for a given endpoint and protocol. + + Args: + endpoint: The OTLP endpoint URL (used for all exporters if individual endpoints not specified). + protocol: The protocol to use ("grpc" or "http"). Default is "grpc". + headers: Optional headers to include in requests (used for all exporters if individual headers not specified). + traces_endpoint: Optional specific endpoint for traces. Overrides endpoint parameter. + traces_headers: Optional specific headers for traces. Overrides headers parameter. + metrics_endpoint: Optional specific endpoint for metrics. Overrides endpoint parameter. + metrics_headers: Optional specific headers for metrics. Overrides headers parameter. + logs_endpoint: Optional specific endpoint for logs. Overrides endpoint parameter. + logs_headers: Optional specific headers for logs. Overrides headers parameter. + + Returns: + List containing OTLPLogExporter, OTLPSpanExporter, and OTLPMetricExporter. + + Raises: + ImportError: If the required OTLP exporter package is not installed. + """ + # Determine actual endpoints and headers to use + actual_traces_endpoint = traces_endpoint or endpoint + actual_metrics_endpoint = metrics_endpoint or endpoint + actual_logs_endpoint = logs_endpoint or endpoint + actual_traces_headers = traces_headers or headers + actual_metrics_headers = metrics_headers or headers + actual_logs_headers = logs_headers or headers exporters: list["LogRecordExporter | SpanExporter | MetricExporter"] = [] - for endpoint in endpoints: - exporters.append(OTLPLogExporter(endpoint=endpoint)) - exporters.append(OTLPSpanExporter(endpoint=endpoint)) - exporters.append(OTLPMetricExporter(endpoint=endpoint)) - return exporters + if not actual_logs_endpoint and not actual_traces_endpoint and not actual_metrics_endpoint: + return exporters + if protocol in ("grpc", "http/protobuf"): + # Import all gRPC exporters + try: + from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter as GRPCLogExporter + from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( + OTLPMetricExporter as GRPCMetricExporter, + ) + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter as GRPCSpanExporter + except ImportError as exc: + raise ImportError( + "opentelemetry-exporter-otlp-proto-grpc is required for OTLP gRPC exporters. " + "Install it with: pip install opentelemetry-exporter-otlp-proto-grpc" + ) from exc + + if actual_logs_endpoint: + exporters.append( + GRPCLogExporter( + endpoint=actual_logs_endpoint, + headers=actual_logs_headers if actual_logs_headers else None, + ) + ) + if actual_traces_endpoint: + exporters.append( + GRPCSpanExporter( + endpoint=actual_traces_endpoint, + headers=actual_traces_headers if actual_traces_headers else None, + ) + ) + if actual_metrics_endpoint: + exporters.append( + GRPCMetricExporter( + endpoint=actual_metrics_endpoint, + headers=actual_metrics_headers if actual_metrics_headers else None, + ) + ) -def _get_azure_monitor_exporters( - connection_strings: list[str], - credential: "TokenCredential | None" = None, -) -> list["LogRecordExporter | SpanExporter | MetricExporter"]: - """Create Azure Monitor Exporters, based on the connection strings and optionally the credential.""" - try: - from azure.monitor.opentelemetry.exporter import ( - AzureMonitorLogExporter, - AzureMonitorMetricExporter, - AzureMonitorTraceExporter, - ) - except ImportError as e: - raise ImportError( - "azure-monitor-opentelemetry-exporter is required for Azure Monitor exporters. " - "Install it with: pip install azure-monitor-opentelemetry-exporter>=1.0.0b41" - ) from e + elif protocol == "http": + # Import all HTTP exporters + try: + from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter as HTTPLogExporter + from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( + OTLPMetricExporter as HTTPMetricExporter, + ) + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter as HTTPSpanExporter + except ImportError as exc: + raise ImportError( + "opentelemetry-exporter-otlp-proto-http is required for OTLP HTTP exporters. " + "Install it with: pip install opentelemetry-exporter-otlp-proto-http" + ) from exc + + if actual_logs_endpoint: + exporters.append( + HTTPLogExporter( + endpoint=actual_logs_endpoint, + headers=actual_logs_headers if actual_logs_headers else None, + ) + ) + if actual_traces_endpoint: + exporters.append( + HTTPSpanExporter( + endpoint=actual_traces_endpoint, + headers=actual_traces_headers if actual_traces_headers else None, + ) + ) + if actual_metrics_endpoint: + exporters.append( + HTTPMetricExporter( + endpoint=actual_metrics_endpoint, + headers=actual_metrics_headers if actual_metrics_headers else None, + ) + ) - exporters: list["LogRecordExporter | SpanExporter | MetricExporter"] = [] - for conn_string in connection_strings: - exporters.append(AzureMonitorLogExporter(connection_string=conn_string, credential=credential)) - exporters.append(AzureMonitorTraceExporter(connection_string=conn_string, credential=credential)) - exporters.append(AzureMonitorMetricExporter(connection_string=conn_string, credential=credential)) return exporters -def get_exporters( - otlp_endpoints: list[str] | None = None, - connection_strings: list[str] | None = None, - credential: "TokenCredential | None" = None, +def _get_exporters_from_env( + env_file_path: str | None = None, + env_file_encoding: str | None = None, ) -> list["LogRecordExporter | SpanExporter | MetricExporter"]: - """Add additional exporters to the existing configuration. + """Parse OpenTelemetry environment variables and create exporters. + + This function reads standard OpenTelemetry environment variables to configure + OTLP exporters for traces, logs, and metrics. + + The following environment variables are supported: + - OTEL_EXPORTER_OTLP_ENDPOINT: Base endpoint for all signals + - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: Endpoint specifically for traces + - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: Endpoint specifically for metrics + - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: Endpoint specifically for logs + - OTEL_EXPORTER_OTLP_PROTOCOL: Protocol to use (grpc, http/protobuf) + - OTEL_EXPORTER_OTLP_HEADERS: Headers for all signals + - OTEL_EXPORTER_OTLP_TRACES_HEADERS: Headers specifically for traces + - OTEL_EXPORTER_OTLP_METRICS_HEADERS: Headers specifically for metrics + - OTEL_EXPORTER_OTLP_LOGS_HEADERS: Headers specifically for logs - If you supply exporters, those will be added to the relevant providers directly. - If you supply endpoints or connection strings, new exporters will be created and added. - OTLP_endpoints will be used to create a `OTLPLogExporter`, `OTLPMetricExporter` and `OTLPSpanExporter` - Connection_strings will be used to create AzureMonitorExporters. + Args: + env_file_path: Path to a .env file to load environment variables from. + Default is None, which loads from '.env' if present. + env_file_encoding: Encoding to use when reading the .env file. + Default is None, which uses the system default encoding. - If a endpoint or connection string is already configured, through the environment variables, it will be skipped. - If you call this method twice with the same additional endpoint or connection string, it will be added twice. + Returns: + List of configured exporters (empty if no relevant env vars are set). - Args: - otlp_endpoints: A list of OpenTelemetry Protocol (OTLP) endpoints. Default is None. - connection_strings: A list of Azure Monitor connection strings. Default is None. - credential: The credential to use for Azure Monitor Entra ID authentication. Default is None. + References: + - https://opentelemetry.io/docs/languages/sdk-configuration/general/ + - https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/ """ - new_exporters: list["LogRecordExporter | SpanExporter | MetricExporter"] = [] - if otlp_endpoints: - new_exporters.extend(_get_otlp_exporters(endpoints=otlp_endpoints)) - - if connection_strings: - new_exporters.extend( - _get_azure_monitor_exporters( - connection_strings=connection_strings, - credential=credential, + # Load environment variables from .env file if present + load_dotenv(dotenv_path=env_file_path, encoding=env_file_encoding) + + # Get base endpoint + base_endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT") + + # Get signal-specific endpoints (these override base endpoint) + traces_endpoint = os.getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") or base_endpoint + metrics_endpoint = os.getenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT") or base_endpoint + logs_endpoint = os.getenv("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT") or base_endpoint + + # Get protocol (default is grpc) + protocol = os.getenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc").lower() + + # Get base headers + base_headers_str = os.getenv("OTEL_EXPORTER_OTLP_HEADERS", "") + base_headers = _parse_headers(base_headers_str) + + # Get signal-specific headers (these merge with base headers) + traces_headers_str = os.getenv("OTEL_EXPORTER_OTLP_TRACES_HEADERS", "") + metrics_headers_str = os.getenv("OTEL_EXPORTER_OTLP_METRICS_HEADERS", "") + logs_headers_str = os.getenv("OTEL_EXPORTER_OTLP_LOGS_HEADERS", "") + + traces_headers = {**base_headers, **_parse_headers(traces_headers_str)} + metrics_headers = {**base_headers, **_parse_headers(metrics_headers_str)} + logs_headers = {**base_headers, **_parse_headers(logs_headers_str)} + + # Create exporters using helper function + return _create_otlp_exporters( + protocol=protocol, + traces_endpoint=traces_endpoint, + traces_headers=traces_headers if traces_headers else None, + metrics_endpoint=metrics_endpoint, + metrics_headers=metrics_headers if metrics_headers else None, + logs_endpoint=logs_endpoint, + logs_headers=logs_headers if logs_headers else None, + ) + + +def create_resource( + service_name: str | None = None, + service_version: str | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + **attributes: Any, +) -> "Resource": + """Create an OpenTelemetry Resource from environment variables and parameters. + + This function reads standard OpenTelemetry environment variables to configure + the resource, which identifies your service in telemetry backends. + + The following environment variables are read: + - OTEL_SERVICE_NAME: The name of the service (defaults to "agent_framework") + - OTEL_SERVICE_VERSION: The version of the service (defaults to package version) + - OTEL_RESOURCE_ATTRIBUTES: Additional resource attributes as key=value pairs + + Args: + service_name: Override the service name. If not provided, reads from + OTEL_SERVICE_NAME environment variable or defaults to "agent_framework". + service_version: Override the service version. If not provided, reads from + OTEL_SERVICE_VERSION environment variable or defaults to the package version. + env_file_path: Path to a .env file to load environment variables from. + Default is None, which loads from '.env' if present. + env_file_encoding: Encoding to use when reading the .env file. + Default is None, which uses the system default encoding. + **attributes: Additional resource attributes to include. These will be merged + with attributes from OTEL_RESOURCE_ATTRIBUTES environment variable. + + Returns: + A configured OpenTelemetry Resource instance. + + Examples: + .. code-block:: python + + from agent_framework.observability import create_resource + + # Use defaults from environment variables + resource = create_resource() + + # Override service name + resource = create_resource(service_name="my_service") + + # Add custom attributes + resource = create_resource( + service_name="my_service", service_version="1.0.0", deployment_environment="production" ) - ) - return new_exporters + # Load from custom .env file + resource = create_resource(env_file_path="config/.env") + """ + # Load environment variables from .env file if present + load_dotenv(dotenv_path=env_file_path, encoding=env_file_encoding) + + # Start with provided attributes + resource_attributes: dict[str, Any] = dict(attributes) + + # Set service name + if service_name is None: + service_name = os.getenv("OTEL_SERVICE_NAME", "agent_framework") + resource_attributes[service_attributes.SERVICE_NAME] = service_name -def _create_resource() -> "Resource": - import os + # Set service version + if service_version is None: + service_version = os.getenv("OTEL_SERVICE_VERSION", version_info) + resource_attributes[service_attributes.SERVICE_VERSION] = service_version - from opentelemetry.sdk.resources import Resource - from opentelemetry.semconv.attributes import service_attributes + # Parse OTEL_RESOURCE_ATTRIBUTES environment variable + # Format: key1=value1,key2=value2 + if resource_attrs_env := os.getenv("OTEL_RESOURCE_ATTRIBUTES"): + resource_attributes.update(_parse_headers(resource_attrs_env)) + return Resource.create(resource_attributes) - service_name = os.getenv("OTEL_SERVICE_NAME", "agent_framework") - return Resource.create({service_attributes.SERVICE_NAME: service_name}) +def create_metric_views() -> list["View"]: + """Create the default OpenTelemetry metric views for Agent Framework.""" + from opentelemetry.sdk.metrics.view import DropAggregation, View + + return [ + # Dropping all enable_instrumentation names except for those starting with "agent_framework" + View(instrument_name="agent_framework*"), + View(instrument_name="gen_ai*"), + View(instrument_name="*", aggregation=DropAggregation()), + ] class ObservabilitySettings(AFBaseSettings): @@ -357,14 +567,12 @@ class ObservabilitySettings(AFBaseSettings): Sensitive events should only be enabled on test and development environments. Keyword Args: - enable_otel: Enable OpenTelemetry diagnostics. Default is False. - Can be set via environment variable ENABLE_OTEL. + enable_instrumentation: Enable OpenTelemetry diagnostics. Default is False. + Can be set via environment variable ENABLE_INSTRUMENTATION. enable_sensitive_data: Enable OpenTelemetry sensitive events. Default is False. Can be set via environment variable ENABLE_SENSITIVE_DATA. - applicationinsights_connection_string: The Azure Monitor connection string. Default is None. - Can be set via environment variable APPLICATIONINSIGHTS_CONNECTION_STRING. - otlp_endpoint: The OpenTelemetry Protocol (OTLP) endpoint. Default is None. - Can be set via environment variable OTLP_ENDPOINT. + enable_console_exporters: Enable console exporters for traces, logs, and metrics. + Default is False. Can be set via environment variable ENABLE_CONSOLE_EXPORTERS. vs_code_extension_port: The port the AI Toolkit or Azure AI Foundry VS Code extensions are listening on. Default is None. Can be set via environment variable VS_CODE_EXTENSION_PORT. @@ -375,33 +583,39 @@ class ObservabilitySettings(AFBaseSettings): from agent_framework import ObservabilitySettings # Using environment variables - # Set ENABLE_OTEL=true - # Set APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=... + # Set ENABLE_INSTRUMENTATION=true + # Set ENABLE_CONSOLE_EXPORTERS=true settings = ObservabilitySettings() # Or passing parameters directly - settings = ObservabilitySettings( - enable_otel=True, applicationinsights_connection_string="InstrumentationKey=..." - ) + settings = ObservabilitySettings(enable_instrumentation=True, enable_console_exporters=True) """ env_prefix: ClassVar[str] = "" - enable_otel: bool = False + enable_instrumentation: bool = False enable_sensitive_data: bool = False - applicationinsights_connection_string: str | list[str] | None = None - otlp_endpoint: str | list[str] | None = None + enable_console_exporters: bool = False vs_code_extension_port: int | None = None - _resource: "Resource" = PrivateAttr(default_factory=_create_resource) + _resource: "Resource" = PrivateAttr() _executed_setup: bool = PrivateAttr(default=False) + def __init__(self, **kwargs: Any) -> None: + """Initialize the settings and create the resource.""" + super().__init__(**kwargs) + # Create resource with env file settings + self._resource = create_resource( + env_file_path=self.env_file_path, + env_file_encoding=self.env_file_encoding, + ) + @property def ENABLED(self) -> bool: """Check if model diagnostics are enabled. Model diagnostics are enabled if either diagnostic is enabled or diagnostic with sensitive events is enabled. """ - return self.enable_otel or self.enable_sensitive_data + return self.enable_instrumentation @property def SENSITIVE_DATA_ENABLED(self) -> bool: @@ -409,27 +623,18 @@ def SENSITIVE_DATA_ENABLED(self) -> bool: Sensitive events are enabled if the diagnostic with sensitive events is enabled. """ - return self.enable_sensitive_data + return self.enable_instrumentation and self.enable_sensitive_data @property def is_setup(self) -> bool: """Check if the setup has been executed.""" return self._executed_setup - @property - def resource(self) -> "Resource": - """Get the resource.""" - return self._resource - - @resource.setter - def resource(self, value: "Resource") -> None: - """Set the resource.""" - self._resource = value - def _configure( self, - credential: "TokenCredential | None" = None, + *, additional_exporters: list["LogRecordExporter | SpanExporter | MetricExporter"] | None = None, + views: list["View"] | None = None, ) -> None: """Configure application-wide observability based on the settings. @@ -438,120 +643,102 @@ def _configure( will have no effect. Args: - credential: The credential to use for Azure Monitor Entra ID authentication. Default is None. additional_exporters: A list of additional exporters to add to the configuration. Default is None. + views: Optional list of OpenTelemetry views for metrics. Default is None. """ if not self.ENABLED or self._executed_setup: return - exporters: list["LogRecordExporter | SpanExporter | MetricExporter"] = additional_exporters or [] - if self.otlp_endpoint: - exporters.extend( - _get_otlp_exporters( - self.otlp_endpoint if isinstance(self.otlp_endpoint, list) else [self.otlp_endpoint] - ) - ) - if self.applicationinsights_connection_string: - exporters.extend( - _get_azure_monitor_exporters( - connection_strings=( - self.applicationinsights_connection_string - if isinstance(self.applicationinsights_connection_string, list) - else [self.applicationinsights_connection_string] - ), - credential=credential, - ) + exporters: list["LogRecordExporter | SpanExporter | MetricExporter"] = [] + + # 1. Add exporters from standard OTEL environment variables + exporters.extend( + _get_exporters_from_env( + env_file_path=self.env_file_path, + env_file_encoding=self.env_file_encoding, ) - self._configure_providers(exporters) - self._executed_setup = True + ) - def check_endpoint_already_configured(self, otlp_endpoint: str) -> bool: - """Check if the endpoint is already configured. + # 2. Add passed-in exporters + if additional_exporters: + exporters.extend(additional_exporters) - Returns: - True if the endpoint is already configured, False otherwise. - """ - if not self.otlp_endpoint: - return False - return otlp_endpoint in (self.otlp_endpoint if isinstance(self.otlp_endpoint, list) else [self.otlp_endpoint]) + # 3. Add console exporters if explicitly enabled + if self.enable_console_exporters: + from opentelemetry.sdk._logs.export import ConsoleLogRecordExporter + from opentelemetry.sdk.metrics.export import ConsoleMetricExporter + from opentelemetry.sdk.trace.export import ConsoleSpanExporter - def check_connection_string_already_configured(self, connection_string: str) -> bool: - """Check if the connection string is already configured. + exporters.extend([ConsoleSpanExporter(), ConsoleLogRecordExporter(), ConsoleMetricExporter()]) - Returns: - True if the connection string is already configured, False otherwise. - """ - if not self.applicationinsights_connection_string: - return False - return connection_string in ( - self.applicationinsights_connection_string - if isinstance(self.applicationinsights_connection_string, list) - else [self.applicationinsights_connection_string] - ) + # 4. Add VS Code extension exporters if port is specified + if self.vs_code_extension_port: + endpoint = f"http://localhost:{self.vs_code_extension_port}" + exporters.extend(_create_otlp_exporters(endpoint=endpoint, protocol="grpc")) + + # 5. Configure providers + self._configure_providers(exporters, views=views) + self._executed_setup = True + + def _configure_providers( + self, + exporters: list["LogRecordExporter | MetricExporter | SpanExporter"], + views: list["View"] | None = None, + ) -> None: + """Configure tracing, logging, events and metrics with the provided exporters. - def _configure_providers(self, exporters: list["LogRecordExporter | MetricExporter | SpanExporter"]) -> None: - """Configure tracing, logging, events and metrics with the provided exporters.""" + Args: + exporters: A list of exporters for logs, metrics and/or spans. + views: Optional list of OpenTelemetry views for metrics. Default is empty list. + """ from opentelemetry._logs import set_logger_provider from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, LogRecordExporter from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import MetricExporter, PeriodicExportingMetricReader - from opentelemetry.sdk.metrics.view import DropAggregation, View from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter + span_exporters: list[SpanExporter] = [] + log_exporters: list[LogRecordExporter] = [] + metric_exporters: list[MetricExporter] = [] + for exp in exporters: + if isinstance(exp, SpanExporter): + span_exporters.append(exp) + if isinstance(exp, LogRecordExporter): + log_exporters.append(exp) + if isinstance(exp, MetricExporter): + metric_exporters.append(exp) + # Tracing - tracer_provider = TracerProvider(resource=self.resource) - trace.set_tracer_provider(tracer_provider) - should_add_console_exporter = True - for exporter in exporters: - if isinstance(exporter, SpanExporter): + if span_exporters: + tracer_provider = TracerProvider(resource=self._resource) + trace.set_tracer_provider(tracer_provider) + for exporter in span_exporters: tracer_provider.add_span_processor(BatchSpanProcessor(exporter)) - should_add_console_exporter = False - if should_add_console_exporter: - from opentelemetry.sdk.trace.export import ConsoleSpanExporter - - tracer_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) # Logging - logger_provider = LoggerProvider(resource=self.resource) - should_add_console_exporter = True - for exporter in exporters: - if isinstance(exporter, LogRecordExporter): - logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter)) - should_add_console_exporter = False - if should_add_console_exporter: - from opentelemetry.sdk._logs.export import ConsoleLogRecordExporter - - logger_provider.add_log_record_processor(BatchLogRecordProcessor(ConsoleLogRecordExporter())) - - # Attach a handler with the provider to the root logger - logger = logging.getLogger() - handler = LoggingHandler(logger_provider=logger_provider) - logger.addHandler(handler) - set_logger_provider(logger_provider) + if log_exporters: + logger_provider = LoggerProvider(resource=self._resource) + for log_exporter in log_exporters: + logger_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter)) + # Attach a handler with the provider to the root logger + logger = logging.getLogger() + handler = LoggingHandler(logger_provider=logger_provider) + logger.addHandler(handler) + set_logger_provider(logger_provider) # metrics - metric_readers = [ - PeriodicExportingMetricReader(exporter, export_interval_millis=5000) - for exporter in exporters - if isinstance(exporter, MetricExporter) - ] - if not metric_readers: - from opentelemetry.sdk.metrics.export import ConsoleMetricExporter - - metric_readers = [PeriodicExportingMetricReader(ConsoleMetricExporter(), export_interval_millis=5000)] - meter_provider = MeterProvider( - metric_readers=metric_readers, - resource=self.resource, - views=[ - # Dropping all instrument names except for those starting with "agent_framework" - View(instrument_name="*", aggregation=DropAggregation()), - View(instrument_name="agent_framework*"), - View(instrument_name="gen_ai*"), - ], - ) - metrics.set_meter_provider(meter_provider) + if metric_exporters: + meter_provider = MeterProvider( + metric_readers=[ + PeriodicExportingMetricReader(exporter, export_interval_millis=5000) + for exporter in metric_exporters + ], + resource=self._resource, + views=views or [], + ) + metrics.set_meter_provider(meter_provider) def get_tracer( @@ -661,125 +848,174 @@ def get_meter( OBSERVABILITY_SETTINGS: ObservabilitySettings = ObservabilitySettings() -def setup_observability( +def enable_instrumentation( + *, + enable_sensitive_data: bool | None = None, +) -> None: + """Enable instrumentation for your application. + + Calling this method implies you want to enable observability in your application. + + This method does not configure exporters or providers. + It only updates the global variables that trigger the instrumentation code. + If you have already set the environment variable ENABLE_INSTRUMENTATION=true, + calling this method has no effect, unless you want to enable or disable sensitive data events. + + Keyword Args: + enable_sensitive_data: Enable OpenTelemetry sensitive events. Overrides + the environment variable ENABLE_SENSITIVE_DATA if set. Default is None. + """ + global OBSERVABILITY_SETTINGS + OBSERVABILITY_SETTINGS.enable_instrumentation = True + if enable_sensitive_data is not None: + OBSERVABILITY_SETTINGS.enable_sensitive_data = enable_sensitive_data + + +def configure_otel_providers( + *, enable_sensitive_data: bool | None = None, - otlp_endpoint: str | list[str] | None = None, - applicationinsights_connection_string: str | list[str] | None = None, - credential: "TokenCredential | None" = None, exporters: list["LogRecordExporter | SpanExporter | MetricExporter"] | None = None, + views: list["View"] | None = None, vs_code_extension_port: int | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, ) -> None: - """Setup observability for the application with OpenTelemetry. + """Configure otel providers and enable instrumentation for the application with OpenTelemetry. This method creates the exporters and providers for the application based on - the provided values and environment variables. + the provided values and environment variables and enables instrumentation. Call this method once during application startup, before any telemetry is captured. DO NOT call this method multiple times, as it may lead to unexpected behavior. + The function automatically reads standard OpenTelemetry environment variables: + - OTEL_EXPORTER_OTLP_ENDPOINT: Base OTLP endpoint for all signals + - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP endpoint for traces + - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: OTLP endpoint for metrics + - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: OTLP endpoint for logs + - OTEL_EXPORTER_OTLP_PROTOCOL: Protocol (grpc/http) + - OTEL_EXPORTER_OTLP_HEADERS: Headers for all signals + - ENABLE_CONSOLE_EXPORTERS: Enable console output for telemetry + Note: - If you have configured the providers manually, calling this method will not - have any effect. The reverse is also true - if you call this method first, - subsequent provider configurations will not take effect. + Since you can only setup one provider per signal type (logs, traces, metrics), + you can choose to use this method and take the exporter and provider that we created. + Alternatively, you can setup the providers yourself, or through another library + (e.g., Azure Monitor) and just call `enable_instrumentation()` to enable instrumentation. - Args: + Note: + By default, the Agent Framework emits metrics with the prefixes `agent_framework` + and `gen_ai` (OpenTelemetry GenAI semantic conventions). You can use the `views` + parameter to filter which metrics are collected and exported. You can also use + the `create_metric_views()` helper function to get default views. + + Keyword Args: enable_sensitive_data: Enable OpenTelemetry sensitive events. Overrides - the environment variable if set. Default is None. - otlp_endpoint: The OpenTelemetry Protocol (OTLP) endpoint. Will be used - to create OTLPLogExporter, OTLPMetricExporter and OTLPSpanExporter. - Default is None. - applicationinsights_connection_string: The Azure Monitor connection string. - Will be used to create AzureMonitorExporters. Default is None. - credential: The credential to use for Azure Monitor Entra ID authentication. + the environment variable ENABLE_SENSITIVE_DATA if set. Default is None. + exporters: A list of custom exporters for logs, metrics or spans, or any combination. + These will be added in addition to exporters configured via environment variables. Default is None. - exporters: A list of exporters for logs, metrics or spans, or any combination. - These will be added directly, allowing complete customization. Default is None. - vs_code_extension_port: The port the AI Toolkit or AzureAI Foundry VS Code + views: Optional list of OpenTelemetry views for metrics configuration. + Views allow filtering and customizing which metrics are collected. + Default is None (empty list). + vs_code_extension_port: The port the AI Toolkit or Azure AI Foundry VS Code extensions are listening on. When set, additional OTEL exporters will be - created with endpoint `http://localhost:{vs_code_extension_port}` unless - already configured. Overrides the environment variable if set. Default is None. + created with endpoint `http://localhost:{vs_code_extension_port}`. + Overrides the environment variable VS_CODE_EXTENSION_PORT if set. Default is None. + env_file_path: An optional path to a .env file to load environment variables from. + Default is None. + env_file_encoding: The encoding to use when loading the .env file. Default is None + which uses the system default encoding. Examples: .. code-block:: python - from agent_framework import setup_observability - - # With environment variables - # Set ENABLE_OTEL=true, OTLP_ENDPOINT=http://localhost:4317 - setup_observability() + from agent_framework.observability import configure_otel_providers - # With parameters (no environment variables) - setup_observability( - enable_sensitive_data=True, - otlp_endpoint="http://localhost:4317", - ) + # Using environment variables (recommended) + # Set ENABLE_INSTRUMENTATION=true + # Set OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 + configure_otel_providers() - # With Azure Monitor - setup_observability( - applicationinsights_connection_string="InstrumentationKey=...", - ) + # Enable console output for debugging + # Set ENABLE_CONSOLE_EXPORTERS=true + configure_otel_providers() # With custom exporters - from opentelemetry.sdk.trace.export import ConsoleSpanExporter + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter + from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter - setup_observability( - exporters=[ConsoleSpanExporter()], - ) - - # Mixed: combine environment variables and parameters - # Environment: OTLP_ENDPOINT=http://localhost:7431 - # Code adds additional endpoint - setup_observability( - enable_sensitive_data=True, - otlp_endpoint="http://localhost:4317", # Both endpoints will be used + configure_otel_providers( + exporters=[ + OTLPSpanExporter(endpoint="http://custom:4317"), + OTLPLogExporter(endpoint="http://custom:4317"), + ], ) # VS Code extension integration - setup_observability( + configure_otel_providers( vs_code_extension_port=4317, # Connects to AI Toolkit ) - """ - global OBSERVABILITY_SETTINGS - # Update the observability settings with the provided values - OBSERVABILITY_SETTINGS.enable_otel = True - if enable_sensitive_data is not None: - OBSERVABILITY_SETTINGS.enable_sensitive_data = enable_sensitive_data - if vs_code_extension_port is not None: - OBSERVABILITY_SETTINGS.vs_code_extension_port = vs_code_extension_port - - # Create exporters, after checking if they are already configured through the env. - new_exporters: list["LogRecordExporter | SpanExporter | MetricExporter"] = exporters or [] - if otlp_endpoint: - if isinstance(otlp_endpoint, str): - otlp_endpoint = [otlp_endpoint] - new_exporters.extend( - _get_otlp_exporters( - endpoints=[ - endpoint - for endpoint in otlp_endpoint - if not OBSERVABILITY_SETTINGS.check_endpoint_already_configured(endpoint) - ] + + # Enable sensitive data logging (development only) + configure_otel_providers( + enable_sensitive_data=True, ) - ) - if applicationinsights_connection_string: - if isinstance(applicationinsights_connection_string, str): - applicationinsights_connection_string = [applicationinsights_connection_string] - new_exporters.extend( - _get_azure_monitor_exporters( - connection_strings=[ - conn_str - for conn_str in applicationinsights_connection_string - if not OBSERVABILITY_SETTINGS.check_connection_string_already_configured(conn_str) + + # With custom metrics views + from opentelemetry.sdk.metrics.view import View + + configure_otel_providers( + views=[ + View(instrument_name="agent_framework*"), + View(instrument_name="gen_ai*"), ], - credential=credential, ) - ) - if OBSERVABILITY_SETTINGS.vs_code_extension_port: - endpoint = f"http://localhost:{OBSERVABILITY_SETTINGS.vs_code_extension_port}" - if not OBSERVABILITY_SETTINGS.check_endpoint_already_configured(endpoint): - new_exporters.extend(_get_otlp_exporters(endpoints=[endpoint])) - OBSERVABILITY_SETTINGS._configure(credential=credential, additional_exporters=new_exporters) # pyright: ignore[reportPrivateUsage] + This example shows how to first setup your providers, + and then ensure Agent Framework emits traces, logs and metrics + + .. code-block:: python + + # when azure monitor is installed + from agent_framework.observability import enable_instrumentation + from azure.monitor.opentelemetry import configure_azure_monitor + + connection_string = "InstrumentationKey=your_instrumentation_key_here;..." + configure_azure_monitor(connection_string=connection_string) + enable_instrumentation() + + References: + - https://opentelemetry.io/docs/languages/sdk-configuration/general/ + - https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/ + """ + global OBSERVABILITY_SETTINGS + if env_file_path: + # Build kwargs, excluding None values + settings_kwargs: dict[str, Any] = { + "enable_instrumentation": True, + "env_file_path": env_file_path, + } + if env_file_encoding is not None: + settings_kwargs["env_file_encoding"] = env_file_encoding + if enable_sensitive_data is not None: + settings_kwargs["enable_sensitive_data"] = enable_sensitive_data + if vs_code_extension_port is not None: + settings_kwargs["vs_code_extension_port"] = vs_code_extension_port + + OBSERVABILITY_SETTINGS = ObservabilitySettings(**settings_kwargs) + else: + # Update the observability settings with the provided values + OBSERVABILITY_SETTINGS.enable_instrumentation = True + if enable_sensitive_data is not None: + OBSERVABILITY_SETTINGS.enable_sensitive_data = enable_sensitive_data + if vs_code_extension_port is not None: + OBSERVABILITY_SETTINGS.vs_code_extension_port = vs_code_extension_port + + OBSERVABILITY_SETTINGS._configure( # type: ignore[reportPrivateUsage] + additional_exporters=exporters, + views=views, + ) # region Chat Client Telemetry @@ -993,7 +1229,7 @@ async def trace_get_streaming_response( return decorator(func) -def use_observability( +def use_instrumentation( chat_client: type[TChatClient], ) -> type[TChatClient]: """Class decorator that enables OpenTelemetry observability for a chat client. @@ -1019,12 +1255,12 @@ def use_observability( Examples: .. code-block:: python - from agent_framework import use_observability, setup_observability + from agent_framework import use_instrumentation, configure_otel_providers from agent_framework import ChatClientProtocol # Decorate a custom chat client class - @use_observability + @use_instrumentation class MyCustomChatClient: OTEL_PROVIDER_NAME = "my_provider" @@ -1038,7 +1274,7 @@ async def get_streaming_response(self, messages, **kwargs): # Setup observability - setup_observability(otlp_endpoint="http://localhost:4317") + configure_otel_providers(otlp_endpoint="http://localhost:4317") # Now all calls will be traced client = MyCustomChatClient() @@ -1082,12 +1318,14 @@ async def get_streaming_response(self, messages, **kwargs): def _trace_agent_run( run_func: Callable[..., Awaitable["AgentRunResponse"]], provider_name: str, + capture_usage: bool = True, ) -> Callable[..., Awaitable["AgentRunResponse"]]: """Decorator to trace chat completion activities. Args: run_func: The function to trace. provider_name: The system name used for Open Telemetry. + capture_usage: Whether to capture token usage as a span attribute. """ @wraps(run_func) @@ -1128,7 +1366,7 @@ async def trace_run( capture_exception(span=span, exception=exception, timestamp=time_ns()) raise else: - attributes = _get_response_attributes(attributes, response) + attributes = _get_response_attributes(attributes, response, capture_usage=capture_usage) _capture_response(span=span, attributes=attributes) if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and response.messages: _capture_messages( @@ -1145,12 +1383,14 @@ async def trace_run( def _trace_agent_run_stream( run_streaming_func: Callable[..., AsyncIterable["AgentRunResponseUpdate"]], provider_name: str, + capture_usage: bool, ) -> Callable[..., AsyncIterable["AgentRunResponseUpdate"]]: """Decorator to trace streaming agent run activities. Args: run_streaming_func: The function to trace. provider_name: The system name used for Open Telemetry. + capture_usage: Whether to capture token usage as a span attribute. """ @wraps(run_streaming_func) @@ -1201,7 +1441,7 @@ async def trace_run_streaming( raise else: response = AgentRunResponse.from_agent_run_response_updates(all_updates) - attributes = _get_response_attributes(attributes, response) + attributes = _get_response_attributes(attributes, response, capture_usage=capture_usage) _capture_response(span=span, attributes=attributes) if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and response.messages: _capture_messages( @@ -1214,9 +1454,11 @@ async def trace_run_streaming( return trace_run_streaming -def use_agent_observability( - agent: type[TAgent], -) -> type[TAgent]: +def use_agent_instrumentation( + agent: type[TAgent] | None = None, + *, + capture_usage: bool = True, +) -> type[TAgent] | Callable[[type[TAgent]], type[TAgent]]: """Class decorator that enables OpenTelemetry observability for an agent. This decorator automatically traces agent run requests, captures events, @@ -1224,12 +1466,17 @@ def use_agent_observability( Note: This decorator must be applied to the agent class itself, not an instance. - The agent class should have a class variable AGENT_SYSTEM_NAME to set the + The agent class should have a class variable AGENT_PROVIDER_NAME to set the proper system name for telemetry. Args: agent: The agent class to enable observability for. + Keyword Args: + capture_usage: Whether to capture token usage as a span attribute. + Defaults to True, set to False when the agent has underlying traces + that already capture token usage to avoid double counting. + Returns: The decorated agent class with observability enabled. @@ -1240,14 +1487,14 @@ def use_agent_observability( Examples: .. code-block:: python - from agent_framework import use_agent_observability, setup_observability + from agent_framework import use_agent_instrumentation, configure_otel_providers from agent_framework._agents import AgentProtocol # Decorate a custom agent class - @use_agent_observability + @use_agent_instrumentation class MyCustomAgent: - AGENT_SYSTEM_NAME = "my_agent_system" + AGENT_PROVIDER_NAME = "my_agent_system" async def run(self, messages=None, *, thread=None, **kwargs): # Your implementation @@ -1259,23 +1506,31 @@ async def run_stream(self, messages=None, *, thread=None, **kwargs): # Setup observability - setup_observability(otlp_endpoint="http://localhost:4317") + configure_otel_providers(otlp_endpoint="http://localhost:4317") # Now all agent runs will be traced agent = MyCustomAgent() response = await agent.run("Perform a task") """ - provider_name = str(getattr(agent, "AGENT_SYSTEM_NAME", "Unknown")) - try: - agent.run = _trace_agent_run(agent.run, provider_name) # type: ignore - except AttributeError as exc: - raise AgentInitializationError(f"The agent {agent.__name__} does not have a run method.", exc) from exc - try: - agent.run_stream = _trace_agent_run_stream(agent.run_stream, provider_name) # type: ignore - except AttributeError as exc: - raise AgentInitializationError(f"The agent {agent.__name__} does not have a run_stream method.", exc) from exc - setattr(agent, OPEN_TELEMETRY_AGENT_MARKER, True) - return agent + + def decorator(agent: type[TAgent]) -> type[TAgent]: + provider_name = str(getattr(agent, "AGENT_PROVIDER_NAME", "Unknown")) + try: + agent.run = _trace_agent_run(agent.run, provider_name, capture_usage=capture_usage) # type: ignore + except AttributeError as exc: + raise AgentInitializationError(f"The agent {agent.__name__} does not have a run method.", exc) from exc + try: + agent.run_stream = _trace_agent_run_stream(agent.run_stream, provider_name, capture_usage=capture_usage) # type: ignore + except AttributeError as exc: + raise AgentInitializationError( + f"The agent {agent.__name__} does not have a run_stream method.", exc + ) from exc + setattr(agent, OPEN_TELEMETRY_AGENT_MARKER, True) + return agent + + if agent is None: + return decorator + return decorator(agent) # region Otel Helpers @@ -1458,26 +1713,32 @@ def _to_otel_part(content: "Contents") -> dict[str, Any] | None: match content.type: case "text": return {"type": "text", "content": content.text} + case "text_reasoning": + return {"type": "reasoning", "content": content.text} + case "uri": + return { + "type": "uri", + "uri": content.uri, + "mime_type": content.media_type, + "modality": content.media_type.split("/")[0] if content.media_type else None, + } + case "data": + return { + "type": "blob", + "content": content.get_data_bytes_as_str(), + "mime_type": content.media_type, + "modality": content.media_type.split("/")[0] if content.media_type else None, + } case "function_call": return {"type": "tool_call", "id": content.call_id, "name": content.name, "arguments": content.arguments} case "function_result": - response: Any | None = None - if content.result: - if isinstance(content.result, list): - res: list[Any] = [] - for item in content.result: # type: ignore - from ._types import BaseContent - - if isinstance(item, BaseContent): - res.append(_to_otel_part(item)) # type: ignore - elif isinstance(item, BaseModel): - res.append(item.model_dump(exclude_none=True)) - else: - res.append(json.dumps(item, default=str)) - response = json.dumps(res, default=str) - else: - response = json.dumps(content.result, default=str) - return {"type": "tool_call_response", "id": content.call_id, "response": response} + from ._types import prepare_function_call_results + + return { + "type": "tool_call_response", + "id": content.call_id, + "response": prepare_function_call_results(content), + } case _: # GenericPart in otel output messages json spec. # just required type, and arbitrary other fields. @@ -1489,6 +1750,8 @@ def _get_response_attributes( attributes: dict[str, Any], response: "ChatResponse | AgentRunResponse", duration: float | None = None, + *, + capture_usage: bool = True, ) -> dict[str, Any]: """Get the response attributes from a response.""" if response.response_id: @@ -1502,7 +1765,7 @@ def _get_response_attributes( attributes[OtelAttr.FINISH_REASONS] = json.dumps([finish_reason.value]) if model_id := getattr(response, "model_id", None): attributes[SpanAttributes.LLM_RESPONSE_MODEL] = model_id - if usage := response.usage_details: + if capture_usage and (usage := response.usage_details): if usage.input_token_count: attributes[OtelAttr.INPUT_TOKENS] = usage.input_token_count if usage.output_token_count: diff --git a/python/packages/core/agent_framework/openai/_assistants_client.py b/python/packages/core/agent_framework/openai/_assistants_client.py index 0f3bb3de63..319ad95231 100644 --- a/python/packages/core/agent_framework/openai/_assistants_client.py +++ b/python/packages/core/agent_framework/openai/_assistants_client.py @@ -40,7 +40,7 @@ prepare_function_call_results, ) from ..exceptions import ServiceInitializationError -from ..observability import use_observability +from ..observability import use_instrumentation from ._shared import OpenAIConfigMixin, OpenAISettings if sys.version_info >= (3, 11): @@ -53,7 +53,7 @@ @use_function_invocation -@use_observability +@use_instrumentation @use_chat_middleware class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient): """OpenAI Assistants client.""" diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py index 73605fadef..9222cae8b3 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/core/agent_framework/openai/_chat_client.py @@ -44,7 +44,7 @@ ServiceInvalidRequestError, ServiceResponseException, ) -from ..observability import use_observability +from ..observability import use_instrumentation from ._exceptions import OpenAIContentFilterException from ._shared import OpenAIBase, OpenAIConfigMixin, OpenAISettings @@ -467,7 +467,7 @@ def service_url(self) -> str: @use_function_invocation -@use_observability +@use_instrumentation @use_chat_middleware class OpenAIChatClient(OpenAIConfigMixin, OpenAIBaseChatClient): """OpenAI Chat completion class.""" diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py index a537884ba4..ecdd7be660 100644 --- a/python/packages/core/agent_framework/openai/_responses_client.py +++ b/python/packages/core/agent_framework/openai/_responses_client.py @@ -64,7 +64,7 @@ ServiceInvalidRequestError, ServiceResponseException, ) -from ..observability import use_observability +from ..observability import use_instrumentation from ._exceptions import OpenAIContentFilterException from ._shared import OpenAIBase, OpenAIConfigMixin, OpenAISettings @@ -1127,7 +1127,7 @@ def _get_metadata_from_response(self, output: Any) -> dict[str, Any]: @use_function_invocation -@use_observability +@use_instrumentation @use_chat_middleware class OpenAIResponsesClient(OpenAIConfigMixin, OpenAIBaseResponsesClient): """OpenAI Responses client class.""" diff --git a/python/packages/core/pyproject.toml b/python/packages/core/pyproject.toml index ba55b9498d..62ef888dba 100644 --- a/python/packages/core/pyproject.toml +++ b/python/packages/core/pyproject.toml @@ -30,7 +30,6 @@ dependencies = [ # telemetry "opentelemetry-api>=1.39.0", "opentelemetry-sdk>=1.39.0", - "opentelemetry-exporter-otlp-proto-grpc>=1.39.0", "opentelemetry-semantic-conventions-ai>=0.4.13", # connectors and functions "openai>=1.99.0", diff --git a/python/packages/core/tests/conftest.py b/python/packages/core/tests/conftest.py index d356e300bb..fd8b93ebc2 100644 --- a/python/packages/core/tests/conftest.py +++ b/python/packages/core/tests/conftest.py @@ -10,7 +10,7 @@ @fixture -def enable_otel(request: Any) -> bool: +def enable_instrumentation(request: Any) -> bool: """Fixture that returns a boolean indicating if Otel is enabled.""" return request.param if hasattr(request, "param") else True @@ -22,20 +22,31 @@ def enable_sensitive_data(request: Any) -> bool: @fixture -def span_exporter(monkeypatch, enable_otel: bool, enable_sensitive_data: bool) -> Generator[SpanExporter]: +def span_exporter(monkeypatch, enable_instrumentation: bool, enable_sensitive_data: bool) -> Generator[SpanExporter]: """Fixture to remove environment variables for ObservabilitySettings.""" env_vars = [ - "ENABLE_OTEL", + "ENABLE_INSTRUMENTATION", "ENABLE_SENSITIVE_DATA", - "OTLP_ENDPOINT", - "APPLICATIONINSIGHTS_CONNECTION_STRING", + "ENABLE_CONSOLE_EXPORTERS", + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + "OTEL_EXPORTER_OTLP_PROTOCOL", + "OTEL_EXPORTER_OTLP_HEADERS", + "OTEL_EXPORTER_OTLP_TRACES_HEADERS", + "OTEL_EXPORTER_OTLP_METRICS_HEADERS", + "OTEL_EXPORTER_OTLP_LOGS_HEADERS", + "OTEL_SERVICE_NAME", + "OTEL_SERVICE_VERSION", + "OTEL_RESOURCE_ATTRIBUTES", ] for key in env_vars: monkeypatch.delenv(key, raising=False) # type: ignore - monkeypatch.setenv("ENABLE_OTEL", str(enable_otel)) # type: ignore - if not enable_otel: + monkeypatch.setenv("ENABLE_INSTRUMENTATION", str(enable_instrumentation)) # type: ignore + if not enable_instrumentation: # we overwrite sensitive data for tests enable_sensitive_data = False monkeypatch.setenv("ENABLE_SENSITIVE_DATA", str(enable_sensitive_data)) # type: ignore @@ -51,15 +62,22 @@ def span_exporter(monkeypatch, enable_otel: bool, enable_sensitive_data: bool) - # recreate observability settings with values from above and no file. observability_settings = observability.ObservabilitySettings(env_file_path="test.env") - observability_settings._configure() # pyright: ignore[reportPrivateUsage] + + # Configure providers manually without calling _configure() to avoid OTLP imports + if enable_instrumentation or enable_sensitive_data: + from opentelemetry.sdk.trace import TracerProvider + + tracer_provider = TracerProvider(resource=observability_settings._resource) + trace.set_tracer_provider(tracer_provider) + monkeypatch.setattr(observability, "OBSERVABILITY_SETTINGS", observability_settings, raising=False) # type: ignore with ( patch("agent_framework.observability.OBSERVABILITY_SETTINGS", observability_settings), - patch("agent_framework.observability.setup_observability"), + patch("agent_framework.observability.configure_otel_providers"), ): exporter = InMemorySpanExporter() - if enable_otel or enable_sensitive_data: + if enable_instrumentation or enable_sensitive_data: tracer_provider = trace.get_tracer_provider() if not hasattr(tracer_provider, "add_span_processor"): raise RuntimeError("Tracer provider does not support adding span processors.") diff --git a/python/packages/core/tests/core/test_observability.py b/python/packages/core/tests/core/test_observability.py index abdc5184be..8528295406 100644 --- a/python/packages/core/tests/core/test_observability.py +++ b/python/packages/core/tests/core/test_observability.py @@ -33,8 +33,8 @@ ChatMessageListTimestampFilter, OtelAttr, get_function_span, - use_agent_observability, - use_observability, + use_agent_instrumentation, + use_instrumentation, ) # region Test constants @@ -157,7 +157,7 @@ def test_start_span_with_tool_call_id(span_exporter: InMemorySpanExporter): assert span.attributes[OtelAttr.TOOL_TYPE] == "function" -# region Test use_observability decorator +# region Test use_instrumentation decorator def test_decorator_with_valid_class(): @@ -175,7 +175,7 @@ async def gen(): return gen() # Apply the decorator - decorated_class = use_observability(MockChatClient) + decorated_class = use_instrumentation(MockChatClient) assert hasattr(decorated_class, OPEN_TELEMETRY_CHAT_CLIENT_MARKER) @@ -187,7 +187,7 @@ class MockChatClient: # Apply the decorator - should not raise an error with pytest.raises(ChatClientInitializationError): - use_observability(MockChatClient) + use_instrumentation(MockChatClient) def test_decorator_with_partial_methods(): @@ -200,7 +200,7 @@ async def get_response(self, messages, **kwargs): return Mock() with pytest.raises(ChatClientInitializationError): - use_observability(MockChatClient) + use_instrumentation(MockChatClient) # region Test telemetry decorator with mock client @@ -235,7 +235,7 @@ async def _inner_get_streaming_response( @pytest.mark.parametrize("enable_sensitive_data", [True, False], indirect=True) async def test_chat_client_observability(mock_chat_client, span_exporter: InMemorySpanExporter, enable_sensitive_data): """Test that when diagnostics are enabled, telemetry is applied.""" - client = use_observability(mock_chat_client)() + client = use_instrumentation(mock_chat_client)() messages = [ChatMessage(role=Role.USER, text="Test message")] span_exporter.clear() @@ -258,8 +258,8 @@ async def test_chat_client_observability(mock_chat_client, span_exporter: InMemo async def test_chat_client_streaming_observability( mock_chat_client, span_exporter: InMemorySpanExporter, enable_sensitive_data ): - """Test streaming telemetry through the use_observability decorator.""" - client = use_observability(mock_chat_client)() + """Test streaming telemetry through the use_instrumentation decorator.""" + client = use_instrumentation(mock_chat_client)() messages = [ChatMessage(role=Role.USER, text="Test")] span_exporter.clear() # Collect all yielded updates @@ -282,7 +282,7 @@ async def test_chat_client_streaming_observability( async def test_chat_client_without_model_id_observability(mock_chat_client, span_exporter: InMemorySpanExporter): """Test telemetry shouldn't fail when the model_id is not provided for unknown reason.""" - client = use_observability(mock_chat_client)() + client = use_instrumentation(mock_chat_client)() messages = [ChatMessage(role=Role.USER, text="Test")] span_exporter.clear() response = await client.get_response(messages=messages) @@ -301,7 +301,7 @@ async def test_chat_client_streaming_without_model_id_observability( mock_chat_client, span_exporter: InMemorySpanExporter ): """Test streaming telemetry shouldn't fail when the model_id is not provided for unknown reason.""" - client = use_observability(mock_chat_client)() + client = use_instrumentation(mock_chat_client)() messages = [ChatMessage(role=Role.USER, text="Test")] span_exporter.clear() # Collect all yielded updates @@ -329,7 +329,7 @@ def test_prepend_user_agent_with_none_value(): assert AGENT_FRAMEWORK_USER_AGENT in str(result["User-Agent"]) -# region Test use_agent_observability decorator +# region Test use_agent_instrumentation decorator def test_agent_decorator_with_valid_class(): @@ -337,7 +337,7 @@ def test_agent_decorator_with_valid_class(): # Create a mock class with the required methods class MockChatClientAgent: - AGENT_SYSTEM_NAME = "test_agent_system" + AGENT_PROVIDER_NAME = "test_agent_system" def __init__(self): self.id = "test_agent_id" @@ -358,7 +358,7 @@ def get_new_thread(self) -> AgentThread: return AgentThread() # Apply the decorator - decorated_class = use_agent_observability(MockChatClientAgent) + decorated_class = use_agent_instrumentation(MockChatClientAgent) assert hasattr(decorated_class, OPEN_TELEMETRY_AGENT_MARKER) @@ -367,19 +367,19 @@ def test_agent_decorator_with_missing_methods(): """Test that agent decorator handles classes missing required methods gracefully.""" class MockAgent: - AGENT_SYSTEM_NAME = "test_agent_system" + AGENT_PROVIDER_NAME = "test_agent_system" # Apply the decorator - should not raise an error with pytest.raises(AgentInitializationError): - use_agent_observability(MockAgent) + use_agent_instrumentation(MockAgent) def test_agent_decorator_with_partial_methods(): """Test agent decorator when only one method is present.""" - from agent_framework.observability import use_agent_observability + from agent_framework.observability import use_agent_instrumentation class MockAgent: - AGENT_SYSTEM_NAME = "test_agent_system" + AGENT_PROVIDER_NAME = "test_agent_system" def __init__(self): self.id = "test_agent_id" @@ -390,7 +390,7 @@ async def run(self, messages=None, *, thread=None, **kwargs): return Mock() with pytest.raises(AgentInitializationError): - use_agent_observability(MockAgent) + use_agent_instrumentation(MockAgent) # region Test agent telemetry decorator with mock agent @@ -401,7 +401,7 @@ def mock_chat_agent(): """Create a mock chat client agent for testing.""" class MockChatClientAgent: - AGENT_SYSTEM_NAME = "test_agent_system" + AGENT_PROVIDER_NAME = "test_agent_system" def __init__(self): self.id = "test_agent_id" @@ -433,7 +433,7 @@ async def test_agent_instrumentation_enabled( ): """Test that when agent diagnostics are enabled, telemetry is applied.""" - agent = use_agent_observability(mock_chat_agent)() + agent = use_agent_instrumentation(mock_chat_agent)() span_exporter.clear() response = await agent.run("Test message") @@ -457,8 +457,8 @@ async def test_agent_instrumentation_enabled( async def test_agent_streaming_response_with_diagnostics_enabled_via_decorator( mock_chat_agent: AgentProtocol, span_exporter: InMemorySpanExporter, enable_sensitive_data ): - """Test agent streaming telemetry through the use_agent_observability decorator.""" - agent = use_agent_observability(mock_chat_agent)() + """Test agent streaming telemetry through the use_agent_instrumentation decorator.""" + agent = use_agent_instrumentation(mock_chat_agent)() span_exporter.clear() updates = [] async for update in agent.run_stream("Test message"): @@ -522,3 +522,393 @@ async def failing_function(param: str) -> str: exception_message = exception_event.attributes["exception.message"] assert isinstance(exception_message, str) assert "Function execution failed" in exception_message + + +# region Test OTEL environment variable parsing + + +@pytest.mark.skipif( + True, + reason="Skipping OTLP exporter tests - optional dependency not installed by default", +) +def test_get_exporters_from_env_with_grpc_endpoint(monkeypatch): + """Test _get_exporters_from_env with OTEL_EXPORTER_OTLP_ENDPOINT (gRPC).""" + from agent_framework.observability import _get_exporters_from_env + + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc") + + exporters = _get_exporters_from_env() + + # Should return 3 exporters (trace, metrics, logs) + assert len(exporters) == 3 + + +@pytest.mark.skipif( + True, + reason="Skipping OTLP exporter tests - optional dependency not installed by default", +) +def test_get_exporters_from_env_with_http_endpoint(monkeypatch): + """Test _get_exporters_from_env with OTEL_EXPORTER_OTLP_ENDPOINT (HTTP).""" + from agent_framework.observability import _get_exporters_from_env + + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http") + + exporters = _get_exporters_from_env() + + # Should return 3 exporters (trace, metrics, logs) + assert len(exporters) == 3 + + +@pytest.mark.skipif( + True, + reason="Skipping OTLP exporter tests - optional dependency not installed by default", +) +def test_get_exporters_from_env_with_individual_endpoints(monkeypatch): + """Test _get_exporters_from_env with individual signal endpoints.""" + from agent_framework.observability import _get_exporters_from_env + + monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://localhost:4317") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://localhost:4318") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "http://localhost:4319") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc") + + exporters = _get_exporters_from_env() + + # Should return 3 exporters (trace, metrics, logs) + assert len(exporters) == 3 + + +@pytest.mark.skipif( + True, + reason="Skipping OTLP exporter tests - optional dependency not installed by default", +) +def test_get_exporters_from_env_with_headers(monkeypatch): + """Test _get_exporters_from_env with OTEL_EXPORTER_OTLP_HEADERS.""" + from agent_framework.observability import _get_exporters_from_env + + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "key1=value1,key2=value2") + + exporters = _get_exporters_from_env() + + # Should return 3 exporters with headers + assert len(exporters) == 3 + + +@pytest.mark.skipif( + True, + reason="Skipping OTLP exporter tests - optional dependency not installed by default", +) +def test_get_exporters_from_env_with_signal_specific_headers(monkeypatch): + """Test _get_exporters_from_env with signal-specific headers.""" + from agent_framework.observability import _get_exporters_from_env + + monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://localhost:4317") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_HEADERS", "trace-key=trace-value") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc") + + exporters = _get_exporters_from_env() + + # Should have at least the traces exporter + assert len(exporters) >= 1 + + +@pytest.mark.skipif( + True, + reason="Skipping OTLP exporter tests - optional dependency not installed by default", +) +def test_get_exporters_from_env_without_env_vars(monkeypatch): + """Test _get_exporters_from_env returns empty list when no env vars set.""" + from agent_framework.observability import _get_exporters_from_env + + # Clear all OTEL env vars + for key in [ + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + ]: + monkeypatch.delenv(key, raising=False) + + exporters = _get_exporters_from_env() + + # Should return empty list + assert len(exporters) == 0 + + +@pytest.mark.skipif( + True, + reason="Skipping OTLP exporter tests - optional dependency not installed by default", +) +def test_get_exporters_from_env_missing_grpc_dependency(monkeypatch): + """Test _get_exporters_from_env raises ImportError when gRPC exporters not installed.""" + + from agent_framework.observability import _get_exporters_from_env + + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc") + + # Mock the import to raise ImportError + original_import = __builtins__.__import__ + + def mock_import(name, *args, **kwargs): + if "opentelemetry.exporter.otlp.proto.grpc" in name: + raise ImportError("No module named 'opentelemetry.exporter.otlp.proto.grpc'") + return original_import(name, *args, **kwargs) + + monkeypatch.setattr(__builtins__, "__import__", mock_import) + + with pytest.raises(ImportError, match="opentelemetry-exporter-otlp-proto-grpc"): + _get_exporters_from_env() + + +# region Test create_resource + + +def test_create_resource_from_env(monkeypatch): + """Test create_resource reads OTEL environment variables.""" + from agent_framework.observability import create_resource + + monkeypatch.setenv("OTEL_SERVICE_NAME", "test-service") + monkeypatch.setenv("OTEL_SERVICE_VERSION", "1.0.0") + monkeypatch.setenv("OTEL_RESOURCE_ATTRIBUTES", "deployment.environment=production,host.name=server1") + + resource = create_resource() + + assert resource.attributes["service.name"] == "test-service" + assert resource.attributes["service.version"] == "1.0.0" + assert resource.attributes["deployment.environment"] == "production" + assert resource.attributes["host.name"] == "server1" + + +def test_create_resource_with_parameters_override_env(monkeypatch): + """Test create_resource parameters override environment variables.""" + from agent_framework.observability import create_resource + + monkeypatch.setenv("OTEL_SERVICE_NAME", "env-service") + monkeypatch.setenv("OTEL_SERVICE_VERSION", "0.1.0") + + resource = create_resource(service_name="param-service", service_version="2.0.0") + + # Parameters should override env vars + assert resource.attributes["service.name"] == "param-service" + assert resource.attributes["service.version"] == "2.0.0" + + +def test_create_resource_with_custom_attributes(monkeypatch): + """Test create_resource accepts custom attributes.""" + from agent_framework.observability import create_resource + + resource = create_resource(custom_attr="custom_value", another_attr=123) + + assert resource.attributes["custom_attr"] == "custom_value" + assert resource.attributes["another_attr"] == 123 + + +# region Test _create_otlp_exporters + + +@pytest.mark.skipif( + True, + reason="Skipping OTLP exporter tests - optional dependency not installed by default", +) +def test_create_otlp_exporters_grpc_with_single_endpoint(): + """Test _create_otlp_exporters creates gRPC exporters with single endpoint.""" + from agent_framework.observability import _create_otlp_exporters + + exporters = _create_otlp_exporters(endpoint="http://localhost:4317", protocol="grpc") + + # Should return 3 exporters (trace, metrics, logs) + assert len(exporters) == 3 + + +@pytest.mark.skipif( + True, + reason="Skipping OTLP exporter tests - optional dependency not installed by default", +) +def test_create_otlp_exporters_http_with_single_endpoint(): + """Test _create_otlp_exporters creates HTTP exporters with single endpoint.""" + from agent_framework.observability import _create_otlp_exporters + + exporters = _create_otlp_exporters(endpoint="http://localhost:4318", protocol="http") + + # Should return 3 exporters (trace, metrics, logs) + assert len(exporters) == 3 + + +@pytest.mark.skipif( + True, + reason="Skipping OTLP exporter tests - optional dependency not installed by default", +) +def test_create_otlp_exporters_with_individual_endpoints(): + """Test _create_otlp_exporters with individual signal endpoints.""" + from agent_framework.observability import _create_otlp_exporters + + exporters = _create_otlp_exporters( + protocol="grpc", + traces_endpoint="http://localhost:4317", + metrics_endpoint="http://localhost:4318", + logs_endpoint="http://localhost:4319", + ) + + # Should return 3 exporters + assert len(exporters) == 3 + + +@pytest.mark.skipif( + True, + reason="Skipping OTLP exporter tests - optional dependency not installed by default", +) +def test_create_otlp_exporters_with_headers(): + """Test _create_otlp_exporters with headers.""" + from agent_framework.observability import _create_otlp_exporters + + exporters = _create_otlp_exporters( + endpoint="http://localhost:4317", protocol="grpc", headers={"Authorization": "Bearer token"} + ) + + # Should return 3 exporters with headers + assert len(exporters) == 3 + + +@pytest.mark.skipif( + True, + reason="Skipping OTLP exporter tests - optional dependency not installed by default", +) +def test_create_otlp_exporters_grpc_missing_dependency(): + """Test _create_otlp_exporters raises ImportError when gRPC exporters not installed.""" + import sys + from unittest.mock import patch + + from agent_framework.observability import _create_otlp_exporters + + # Mock the import to raise ImportError + with ( + patch.dict(sys.modules, {"opentelemetry.exporter.otlp.proto.grpc.trace_exporter": None}), + pytest.raises(ImportError, match="opentelemetry-exporter-otlp-proto-grpc"), + ): + _create_otlp_exporters(endpoint="http://localhost:4317", protocol="grpc") + + +# region Test configure_otel_providers with views + + +@pytest.mark.skipif( + True, + reason="Skipping OTLP exporter tests - optional dependency not installed by default", +) +def test_configure_otel_providers_with_views(monkeypatch): + """Test configure_otel_providers accepts views parameter.""" + from opentelemetry.sdk.metrics import View + from opentelemetry.sdk.metrics.view import DropAggregation + + from agent_framework.observability import configure_otel_providers + + # Clear all OTEL env vars + for key in [ + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + ]: + monkeypatch.delenv(key, raising=False) + + # Create a view that drops all metrics + views = [View(instrument_name="*", aggregation=DropAggregation())] + + # Should not raise an error + configure_otel_providers(views=views) + + +@pytest.mark.skipif( + True, + reason="Skipping OTLP exporter tests - optional dependency not installed by default", +) +def test_configure_otel_providers_without_views(monkeypatch): + """Test configure_otel_providers works without views parameter.""" + from agent_framework.observability import configure_otel_providers + + # Clear all OTEL env vars + for key in [ + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + ]: + monkeypatch.delenv(key, raising=False) + + # Should not raise an error with default empty views + configure_otel_providers() + + +# region Test console exporters opt-in + + +def test_console_exporters_opt_in_false(monkeypatch): + """Test console exporters are not added when ENABLE_CONSOLE_EXPORTERS is false.""" + from agent_framework.observability import ObservabilitySettings + + monkeypatch.setenv("ENABLE_CONSOLE_EXPORTERS", "false") + monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) + + settings = ObservabilitySettings(env_file_path="test.env") + assert settings.enable_console_exporters is False + + +def test_console_exporters_opt_in_true(monkeypatch): + """Test console exporters are added when ENABLE_CONSOLE_EXPORTERS is true.""" + from agent_framework.observability import ObservabilitySettings + + monkeypatch.setenv("ENABLE_CONSOLE_EXPORTERS", "true") + + settings = ObservabilitySettings(env_file_path="test.env") + assert settings.enable_console_exporters is True + + +def test_console_exporters_default_false(monkeypatch): + """Test console exporters default to False when not set.""" + from agent_framework.observability import ObservabilitySettings + + monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) + + settings = ObservabilitySettings(env_file_path="test.env") + assert settings.enable_console_exporters is False + + +# region Test _parse_headers helper + + +def test_parse_headers_valid(): + """Test _parse_headers with valid header string.""" + from agent_framework.observability import _parse_headers + + headers = _parse_headers("key1=value1,key2=value2") + assert headers == {"key1": "value1", "key2": "value2"} + + +def test_parse_headers_with_spaces(): + """Test _parse_headers handles spaces around keys and values.""" + from agent_framework.observability import _parse_headers + + headers = _parse_headers("key1 = value1 , key2 = value2 ") + assert headers == {"key1": "value1", "key2": "value2"} + + +def test_parse_headers_empty_string(): + """Test _parse_headers with empty string.""" + from agent_framework.observability import _parse_headers + + headers = _parse_headers("") + assert headers == {} + + +def test_parse_headers_invalid_format(): + """Test _parse_headers ignores invalid pairs.""" + from agent_framework.observability import _parse_headers + + headers = _parse_headers("key1=value1,invalid,key2=value2") + # Should only include valid pairs + assert headers == {"key1": "value1", "key2": "value2"} diff --git a/python/packages/core/tests/openai/test_openai_responses_client.py b/python/packages/core/tests/openai/test_openai_responses_client.py index cc187e01f2..3863f4701a 100644 --- a/python/packages/core/tests/openai/test_openai_responses_client.py +++ b/python/packages/core/tests/openai/test_openai_responses_client.py @@ -832,7 +832,7 @@ def test_create_streaming_response_content_with_mcp_approval_request() -> None: assert fa.function_call.name == "do_stream_action" -@pytest.mark.parametrize("enable_otel", [False], indirect=True) +@pytest.mark.parametrize("enable_instrumentation", [False], indirect=True) @pytest.mark.parametrize("enable_sensitive_data", [False], indirect=True) async def test_end_to_end_mcp_approval_flow(span_exporter) -> None: """End-to-end mocked test: diff --git a/python/packages/core/tests/test_observability_datetime.py b/python/packages/core/tests/test_observability_datetime.py index 05efdc1a5e..6ad3d77e1a 100644 --- a/python/packages/core/tests/test_observability_datetime.py +++ b/python/packages/core/tests/test_observability_datetime.py @@ -22,5 +22,5 @@ def test_datetime_in_tool_results() -> None: result = _to_otel_part(content) parsed = json.loads(result["response"]) - # Datetime should be converted to string - assert isinstance(parsed["timestamp"], str) + # Datetime should be converted to string in the result field + assert isinstance(parsed["result"]["timestamp"], str) diff --git a/python/packages/core/tests/workflow/test_workflow_observability.py b/python/packages/core/tests/workflow/test_workflow_observability.py index 1760361f1a..4c97b850b8 100644 --- a/python/packages/core/tests/workflow/test_workflow_observability.py +++ b/python/packages/core/tests/workflow/test_workflow_observability.py @@ -229,8 +229,10 @@ async def test_trace_context_handling(span_exporter: InMemorySpanExporter) -> No assert processing_span.attributes.get("message.payload_type") == "str" -@pytest.mark.parametrize("enable_otel", [False], indirect=True) -async def test_trace_context_disabled_when_tracing_disabled(enable_otel, span_exporter: InMemorySpanExporter) -> None: +@pytest.mark.parametrize("enable_instrumentation", [False], indirect=True) +async def test_trace_context_disabled_when_tracing_disabled( + enable_instrumentation, span_exporter: InMemorySpanExporter +) -> None: """Test that no trace context is added when tracing is disabled.""" # Tracing should be disabled by default executor = MockExecutor("test-executor") @@ -433,7 +435,7 @@ async def handle_message(self, message: str, ctx: WorkflowContext) -> None: assert workflow_span.status.status_code.name == "ERROR" -@pytest.mark.parametrize("enable_otel", [False], indirect=True) +@pytest.mark.parametrize("enable_instrumentation", [False], indirect=True) async def test_message_trace_context_serialization(span_exporter: InMemorySpanExporter) -> None: """Test that message trace context is properly serialized/deserialized.""" ctx = InProcRunnerContext(InMemoryCheckpointStorage()) diff --git a/python/packages/devui/agent_framework_devui/__init__.py b/python/packages/devui/agent_framework_devui/__init__.py index 45d1ea8c2d..9a480d170e 100644 --- a/python/packages/devui/agent_framework_devui/__init__.py +++ b/python/packages/devui/agent_framework_devui/__init__.py @@ -177,9 +177,9 @@ def serve( import os # Only set if not already configured by user - if not os.environ.get("ENABLE_OTEL"): - os.environ["ENABLE_OTEL"] = "true" - logger.info("Set ENABLE_OTEL=true for tracing") + if not os.environ.get("ENABLE_INSTRUMENTATION"): + os.environ["ENABLE_INSTRUMENTATION"] = "true" + logger.info("Set ENABLE_INSTRUMENTATION=true for tracing") if not os.environ.get("ENABLE_SENSITIVE_DATA"): os.environ["ENABLE_SENSITIVE_DATA"] = "true" diff --git a/python/packages/devui/agent_framework_devui/_executor.py b/python/packages/devui/agent_framework_devui/_executor.py index 813bc4d4cc..be769cba09 100644 --- a/python/packages/devui/agent_framework_devui/_executor.py +++ b/python/packages/devui/agent_framework_devui/_executor.py @@ -82,27 +82,23 @@ def _setup_tracing_provider(self) -> None: def _setup_agent_framework_tracing(self) -> None: """Set up Agent Framework's built-in tracing.""" - # Configure Agent Framework tracing only if ENABLE_OTEL is set - if os.environ.get("ENABLE_OTEL"): + # Configure Agent Framework tracing only if ENABLE_INSTRUMENTATION is set + if os.environ.get("ENABLE_INSTRUMENTATION"): try: - from agent_framework.observability import OBSERVABILITY_SETTINGS, setup_observability + from agent_framework.observability import OBSERVABILITY_SETTINGS, configure_otel_providers # Only configure if not already executed if not OBSERVABILITY_SETTINGS._executed_setup: - # Get OTLP endpoint from either custom or standard env var - # This handles the case where env vars are set after ObservabilitySettings was imported - otlp_endpoint = os.environ.get("OTLP_ENDPOINT") or os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") - - # Pass the endpoint explicitly to setup_observability + # Run the configure_otel_providers # This ensures OTLP exporters are created even if env vars were set late - setup_observability(enable_sensitive_data=True, otlp_endpoint=otlp_endpoint) + configure_otel_providers(enable_sensitive_data=True) logger.info("Enabled Agent Framework observability") else: logger.debug("Agent Framework observability already configured") except Exception as e: logger.warning(f"Failed to enable Agent Framework observability: {e}") else: - logger.debug("ENABLE_OTEL not set, skipping observability setup") + logger.debug("ENABLE_INSTRUMENTATION not set, skipping observability setup") async def discover_entities(self) -> list[EntityInfo]: """Discover all available entities. diff --git a/python/packages/devui/agent_framework_devui/_server.py b/python/packages/devui/agent_framework_devui/_server.py index 26630945cb..b3a7c751b6 100644 --- a/python/packages/devui/agent_framework_devui/_server.py +++ b/python/packages/devui/agent_framework_devui/_server.py @@ -407,7 +407,7 @@ async def get_meta() -> MetaResponse: framework="agent_framework", runtime="python", # Python DevUI backend capabilities={ - "tracing": os.getenv("ENABLE_OTEL") == "true", + "tracing": os.getenv("ENABLE_INSTRUMENTATION") == "true", "openai_proxy": openai_executor.is_configured, "deployment": True, # Deployment feature is available }, diff --git a/python/packages/devui/agent_framework_devui/ui/assets/index.js b/python/packages/devui/agent_framework_devui/ui/assets/index.js index d12b71a838..1b05f27842 100644 --- a/python/packages/devui/agent_framework_devui/ui/assets/index.js +++ b/python/packages/devui/agent_framework_devui/ui/assets/index.js @@ -1,4 +1,4 @@ -function yE(e,n){for(var r=0;ra[l]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))a(l);new MutationObserver(l=>{for(const c of l)if(c.type==="childList")for(const d of c.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&a(d)}).observe(document,{childList:!0,subtree:!0});function r(l){const c={};return l.integrity&&(c.integrity=l.integrity),l.referrerPolicy&&(c.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?c.credentials="include":l.crossOrigin==="anonymous"?c.credentials="omit":c.credentials="same-origin",c}function a(l){if(l.ep)return;l.ep=!0;const c=r(l);fetch(l.href,c)}})();function yp(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Gm={exports:{}},Bi={};/** +function yE(e, n) { for (var r = 0; r < n.length; r++) { const a = n[r]; if (typeof a != "string" && !Array.isArray(a)) { for (const l in a) if (l !== "default" && !(l in e)) { const c = Object.getOwnPropertyDescriptor(a, l); c && Object.defineProperty(e, l, c.get ? c : { enumerable: !0, get: () => a[l] }) } } } return Object.freeze(Object.defineProperty(e, Symbol.toStringTag, { value: "Module" })) } (function () { const n = document.createElement("link").relList; if (n && n.supports && n.supports("modulepreload")) return; for (const l of document.querySelectorAll('link[rel="modulepreload"]')) a(l); new MutationObserver(l => { for (const c of l) if (c.type === "childList") for (const d of c.addedNodes) d.tagName === "LINK" && d.rel === "modulepreload" && a(d) }).observe(document, { childList: !0, subtree: !0 }); function r(l) { const c = {}; return l.integrity && (c.integrity = l.integrity), l.referrerPolicy && (c.referrerPolicy = l.referrerPolicy), l.crossOrigin === "use-credentials" ? c.credentials = "include" : l.crossOrigin === "anonymous" ? c.credentials = "omit" : c.credentials = "same-origin", c } function a(l) { if (l.ep) return; l.ep = !0; const c = r(l); fetch(l.href, c) } })(); function yp(e) { return e && e.__esModule && Object.prototype.hasOwnProperty.call(e, "default") ? e.default : e } var Gm = { exports: {} }, Bi = {};/** * @license React * react-jsx-runtime.production.js * @@ -6,7 +6,7 @@ function yE(e,n){for(var r=0;r>>1,C=k[H];if(0>>1;H<$;){var Y=2*(H+1)-1,V=k[Y],W=Y+1,fe=k[W];if(0>l(V,I))Wl(fe,V)?(k[H]=fe,k[W]=I,H=W):(k[H]=V,k[Y]=I,H=Y);else if(Wl(fe,I))k[H]=fe,k[W]=I,H=W;else break e}}return L}function l(k,L){var I=k.sortIndex-L.sortIndex;return I!==0?I:k.id-L.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var c=performance;e.unstable_now=function(){return c.now()}}else{var d=Date,f=d.now();e.unstable_now=function(){return d.now()-f}}var m=[],h=[],g=1,y=null,x=3,b=!1,S=!1,N=!1,j=!1,_=typeof setTimeout=="function"?setTimeout:null,M=typeof clearTimeout=="function"?clearTimeout:null,E=typeof setImmediate<"u"?setImmediate:null;function T(k){for(var L=r(h);L!==null;){if(L.callback===null)a(h);else if(L.startTime<=k)a(h),L.sortIndex=L.expirationTime,n(m,L);else break;L=r(h)}}function R(k){if(N=!1,T(k),!S)if(r(m)!==null)S=!0,D||(D=!0,G());else{var L=r(h);L!==null&&U(R,L.startTime-k)}}var D=!1,O=-1,B=5,q=-1;function K(){return j?!0:!(e.unstable_now()-qk&&K());){var H=y.callback;if(typeof H=="function"){y.callback=null,x=y.priorityLevel;var C=H(y.expirationTime<=k);if(k=e.unstable_now(),typeof C=="function"){y.callback=C,T(k),L=!0;break t}y===r(m)&&a(m),T(k)}else a(m);y=r(m)}if(y!==null)L=!0;else{var $=r(h);$!==null&&U(R,$.startTime-k),L=!1}}break e}finally{y=null,x=I,b=!1}L=void 0}}finally{L?G():D=!1}}}var G;if(typeof E=="function")G=function(){E(J)};else if(typeof MessageChannel<"u"){var Z=new MessageChannel,P=Z.port2;Z.port1.onmessage=J,G=function(){P.postMessage(null)}}else G=function(){_(J,0)};function U(k,L){O=_(function(){k(e.unstable_now())},L)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(k){k.callback=null},e.unstable_forceFrameRate=function(k){0>k||125H?(k.sortIndex=I,n(h,k),r(m)===null&&k===r(h)&&(N?(M(O),O=-1):N=!0,U(R,I-H))):(k.sortIndex=C,n(m,k),S||b||(S=!0,D||(D=!0,G()))),k},e.unstable_shouldYield=K,e.unstable_wrapCallback=function(k){var L=x;return function(){var I=x;x=L;try{return k.apply(this,arguments)}finally{x=I}}}})(Km)),Km}var tv;function SE(){return tv||(tv=1,Wm.exports=NE()),Wm.exports}var Qm={exports:{}},Wt={};/** + */var ev; function NE() { return ev || (ev = 1, (function (e) { function n(k, L) { var I = k.length; k.push(L); e: for (; 0 < I;) { var H = I - 1 >>> 1, C = k[H]; if (0 < l(C, L)) k[H] = L, k[I] = C, I = H; else break e } } function r(k) { return k.length === 0 ? null : k[0] } function a(k) { if (k.length === 0) return null; var L = k[0], I = k.pop(); if (I !== L) { k[0] = I; e: for (var H = 0, C = k.length, $ = C >>> 1; H < $;) { var Y = 2 * (H + 1) - 1, V = k[Y], W = Y + 1, fe = k[W]; if (0 > l(V, I)) W < C && 0 > l(fe, V) ? (k[H] = fe, k[W] = I, H = W) : (k[H] = V, k[Y] = I, H = Y); else if (W < C && 0 > l(fe, I)) k[H] = fe, k[W] = I, H = W; else break e } } return L } function l(k, L) { var I = k.sortIndex - L.sortIndex; return I !== 0 ? I : k.id - L.id } if (e.unstable_now = void 0, typeof performance == "object" && typeof performance.now == "function") { var c = performance; e.unstable_now = function () { return c.now() } } else { var d = Date, f = d.now(); e.unstable_now = function () { return d.now() - f } } var m = [], h = [], g = 1, y = null, x = 3, b = !1, S = !1, N = !1, j = !1, _ = typeof setTimeout == "function" ? setTimeout : null, M = typeof clearTimeout == "function" ? clearTimeout : null, E = typeof setImmediate < "u" ? setImmediate : null; function T(k) { for (var L = r(h); L !== null;) { if (L.callback === null) a(h); else if (L.startTime <= k) a(h), L.sortIndex = L.expirationTime, n(m, L); else break; L = r(h) } } function R(k) { if (N = !1, T(k), !S) if (r(m) !== null) S = !0, D || (D = !0, G()); else { var L = r(h); L !== null && U(R, L.startTime - k) } } var D = !1, O = -1, B = 5, q = -1; function K() { return j ? !0 : !(e.unstable_now() - q < B) } function J() { if (j = !1, D) { var k = e.unstable_now(); q = k; var L = !0; try { e: { S = !1, N && (N = !1, M(O), O = -1), b = !0; var I = x; try { t: { for (T(k), y = r(m); y !== null && !(y.expirationTime > k && K());) { var H = y.callback; if (typeof H == "function") { y.callback = null, x = y.priorityLevel; var C = H(y.expirationTime <= k); if (k = e.unstable_now(), typeof C == "function") { y.callback = C, T(k), L = !0; break t } y === r(m) && a(m), T(k) } else a(m); y = r(m) } if (y !== null) L = !0; else { var $ = r(h); $ !== null && U(R, $.startTime - k), L = !1 } } break e } finally { y = null, x = I, b = !1 } L = void 0 } } finally { L ? G() : D = !1 } } } var G; if (typeof E == "function") G = function () { E(J) }; else if (typeof MessageChannel < "u") { var Z = new MessageChannel, P = Z.port2; Z.port1.onmessage = J, G = function () { P.postMessage(null) } } else G = function () { _(J, 0) }; function U(k, L) { O = _(function () { k(e.unstable_now()) }, L) } e.unstable_IdlePriority = 5, e.unstable_ImmediatePriority = 1, e.unstable_LowPriority = 4, e.unstable_NormalPriority = 3, e.unstable_Profiling = null, e.unstable_UserBlockingPriority = 2, e.unstable_cancelCallback = function (k) { k.callback = null }, e.unstable_forceFrameRate = function (k) { 0 > k || 125 < k ? console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported") : B = 0 < k ? Math.floor(1e3 / k) : 5 }, e.unstable_getCurrentPriorityLevel = function () { return x }, e.unstable_next = function (k) { switch (x) { case 1: case 2: case 3: var L = 3; break; default: L = x }var I = x; x = L; try { return k() } finally { x = I } }, e.unstable_requestPaint = function () { j = !0 }, e.unstable_runWithPriority = function (k, L) { switch (k) { case 1: case 2: case 3: case 4: case 5: break; default: k = 3 }var I = x; x = k; try { return L() } finally { x = I } }, e.unstable_scheduleCallback = function (k, L, I) { var H = e.unstable_now(); switch (typeof I == "object" && I !== null ? (I = I.delay, I = typeof I == "number" && 0 < I ? H + I : H) : I = H, k) { case 1: var C = -1; break; case 2: C = 250; break; case 5: C = 1073741823; break; case 4: C = 1e4; break; default: C = 5e3 }return C = I + C, k = { id: g++, callback: L, priorityLevel: k, startTime: I, expirationTime: C, sortIndex: -1 }, I > H ? (k.sortIndex = I, n(h, k), r(m) === null && k === r(h) && (N ? (M(O), O = -1) : N = !0, U(R, I - H))) : (k.sortIndex = C, n(m, k), S || b || (S = !0, D || (D = !0, G()))), k }, e.unstable_shouldYield = K, e.unstable_wrapCallback = function (k) { var L = x; return function () { var I = x; x = L; try { return k.apply(this, arguments) } finally { x = I } } } })(Km)), Km } var tv; function SE() { return tv || (tv = 1, Wm.exports = NE()), Wm.exports } var Qm = { exports: {} }, Wt = {};/** * @license React * react-dom.production.js * @@ -30,7 +30,7 @@ function yE(e,n){for(var r=0;r"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(n){console.error(n)}}return e(),Qm.exports=jE(),Qm.exports}/** + */var nv; function jE() { if (nv) return Wt; nv = 1; var e = pl(); function n(m) { var h = "https://react.dev/errors/" + m; if (1 < arguments.length) { h += "?args[]=" + encodeURIComponent(arguments[1]); for (var g = 2; g < arguments.length; g++)h += "&args[]=" + encodeURIComponent(arguments[g]) } return "Minified React error #" + m + "; visit " + h + " for the full message or use the non-minified dev environment for full errors and additional helpful warnings." } function r() { } var a = { d: { f: r, r: function () { throw Error(n(522)) }, D: r, C: r, L: r, m: r, X: r, S: r, M: r }, p: 0, findDOMNode: null }, l = Symbol.for("react.portal"); function c(m, h, g) { var y = 3 < arguments.length && arguments[3] !== void 0 ? arguments[3] : null; return { $$typeof: l, key: y == null ? null : "" + y, children: m, containerInfo: h, implementation: g } } var d = e.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; function f(m, h) { if (m === "font") return ""; if (typeof h == "string") return h === "use-credentials" ? h : "" } return Wt.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = a, Wt.createPortal = function (m, h) { var g = 2 < arguments.length && arguments[2] !== void 0 ? arguments[2] : null; if (!h || h.nodeType !== 1 && h.nodeType !== 9 && h.nodeType !== 11) throw Error(n(299)); return c(m, h, null, g) }, Wt.flushSync = function (m) { var h = d.T, g = a.p; try { if (d.T = null, a.p = 2, m) return m() } finally { d.T = h, a.p = g, a.d.f() } }, Wt.preconnect = function (m, h) { typeof m == "string" && (h ? (h = h.crossOrigin, h = typeof h == "string" ? h === "use-credentials" ? h : "" : void 0) : h = null, a.d.C(m, h)) }, Wt.prefetchDNS = function (m) { typeof m == "string" && a.d.D(m) }, Wt.preinit = function (m, h) { if (typeof m == "string" && h && typeof h.as == "string") { var g = h.as, y = f(g, h.crossOrigin), x = typeof h.integrity == "string" ? h.integrity : void 0, b = typeof h.fetchPriority == "string" ? h.fetchPriority : void 0; g === "style" ? a.d.S(m, typeof h.precedence == "string" ? h.precedence : void 0, { crossOrigin: y, integrity: x, fetchPriority: b }) : g === "script" && a.d.X(m, { crossOrigin: y, integrity: x, fetchPriority: b, nonce: typeof h.nonce == "string" ? h.nonce : void 0 }) } }, Wt.preinitModule = function (m, h) { if (typeof m == "string") if (typeof h == "object" && h !== null) { if (h.as == null || h.as === "script") { var g = f(h.as, h.crossOrigin); a.d.M(m, { crossOrigin: g, integrity: typeof h.integrity == "string" ? h.integrity : void 0, nonce: typeof h.nonce == "string" ? h.nonce : void 0 }) } } else h == null && a.d.M(m) }, Wt.preload = function (m, h) { if (typeof m == "string" && typeof h == "object" && h !== null && typeof h.as == "string") { var g = h.as, y = f(g, h.crossOrigin); a.d.L(m, g, { crossOrigin: y, integrity: typeof h.integrity == "string" ? h.integrity : void 0, nonce: typeof h.nonce == "string" ? h.nonce : void 0, type: typeof h.type == "string" ? h.type : void 0, fetchPriority: typeof h.fetchPriority == "string" ? h.fetchPriority : void 0, referrerPolicy: typeof h.referrerPolicy == "string" ? h.referrerPolicy : void 0, imageSrcSet: typeof h.imageSrcSet == "string" ? h.imageSrcSet : void 0, imageSizes: typeof h.imageSizes == "string" ? h.imageSizes : void 0, media: typeof h.media == "string" ? h.media : void 0 }) } }, Wt.preloadModule = function (m, h) { if (typeof m == "string") if (h) { var g = f(h.as, h.crossOrigin); a.d.m(m, { as: typeof h.as == "string" && h.as !== "script" ? h.as : void 0, crossOrigin: g, integrity: typeof h.integrity == "string" ? h.integrity : void 0 }) } else a.d.m(m) }, Wt.requestFormReset = function (m) { a.d.r(m) }, Wt.unstable_batchedUpdates = function (m, h) { return m(h) }, Wt.useFormState = function (m, h, g) { return d.H.useFormState(m, h, g) }, Wt.useFormStatus = function () { return d.H.useHostTransitionStatus() }, Wt.version = "19.1.1", Wt } var sv; function ew() { if (sv) return Qm.exports; sv = 1; function e() { if (!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ > "u" || typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE != "function")) try { __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e) } catch (n) { console.error(n) } } return e(), Qm.exports = jE(), Qm.exports }/** * @license React * react-dom-client.production.js * @@ -38,414 +38,475 @@ function yE(e,n){for(var r=0;rC||(t.current=H[C],H[C]=null,C--)}function V(t,s){C++,H[C]=t.current,t.current=s}var W=$(null),fe=$(null),ue=$(null),te=$(null);function ie(t,s){switch(V(ue,s),V(fe,t),V(W,null),s.nodeType){case 9:case 11:t=(t=s.documentElement)&&(t=t.namespaceURI)?jy(t):0;break;default:if(t=s.tagName,s=s.namespaceURI)s=jy(s),t=_y(s,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}Y(W),V(W,t)}function ge(){Y(W),Y(fe),Y(ue)}function be(t){t.memoizedState!==null&&V(te,t);var s=W.current,i=_y(s,t.type);s!==i&&(V(fe,t),V(W,i))}function we(t){fe.current===t&&(Y(W),Y(fe)),te.current===t&&(Y(te),zi._currentValue=I)}var ne=Object.prototype.hasOwnProperty,pe=e.unstable_scheduleCallback,he=e.unstable_cancelCallback,ee=e.unstable_shouldYield,ve=e.unstable_requestPaint,ye=e.unstable_now,Te=e.unstable_getCurrentPriorityLevel,je=e.unstable_ImmediatePriority,$e=e.unstable_UserBlockingPriority,it=e.unstable_NormalPriority,ze=e.unstable_LowPriority,Se=e.unstable_IdlePriority,Pe=e.log,Ee=e.unstable_setDisableYieldValue,He=null,Fe=null;function Nt(t){if(typeof Pe=="function"&&Ee(t),Fe&&typeof Fe.setStrictMode=="function")try{Fe.setStrictMode(He,t)}catch{}}var yt=Math.clz32?Math.clz32:xe,hs=Math.log,wo=Math.LN2;function xe(t){return t>>>=0,t===0?32:31-(hs(t)/wo|0)|0}var Re=256,Ue=4194304;function Et(t){var s=t&42;if(s!==0)return s;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t&4194048;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function Dn(t,s,i){var u=t.pendingLanes;if(u===0)return 0;var p=0,v=t.suspendedLanes,A=t.pingedLanes;t=t.warmLanes;var z=u&134217727;return z!==0?(u=z&~v,u!==0?p=Et(u):(A&=z,A!==0?p=Et(A):i||(i=z&~t,i!==0&&(p=Et(i))))):(z=u&~v,z!==0?p=Et(z):A!==0?p=Et(A):i||(i=u&~t,i!==0&&(p=Et(i)))),p===0?0:s!==0&&s!==p&&(s&v)===0&&(v=p&-p,i=s&-s,v>=i||v===32&&(i&4194048)!==0)?s:p}function Le(t,s){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&s)===0}function Ne(t,s){switch(t){case 1:case 2:case 4:case 8:case 64:return s+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return s+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function lt(){var t=Re;return Re<<=1,(Re&4194048)===0&&(Re=256),t}function ot(){var t=Ue;return Ue<<=1,(Ue&62914560)===0&&(Ue=4194304),t}function At(t){for(var s=[],i=0;31>i;i++)s.push(t);return s}function en(t,s){t.pendingLanes|=s,s!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function On(t,s,i,u,p,v){var A=t.pendingLanes;t.pendingLanes=i,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=i,t.entangledLanes&=i,t.errorRecoveryDisabledLanes&=i,t.shellSuspendCounter=0;var z=t.entanglements,F=t.expirationTimes,re=t.hiddenUpdates;for(i=A&~i;0)":-1p||F[u]!==re[p]){var le=` -`+F[u].replace(" at new "," at ");return t.displayName&&le.includes("")&&(le=le.replace("",t.displayName)),le}while(1<=u&&0<=p);break}}}finally{qa=!1,Error.prepareStackTrace=i}return(i=t?t.displayName||t.name:"")?ys(i):""}function Ud(t){switch(t.tag){case 26:case 27:case 5:return ys(t.type);case 16:return ys("Lazy");case 13:return ys("Suspense");case 19:return ys("SuspenseList");case 0:case 15:return Fa(t.type,!1);case 11:return Fa(t.type.render,!1);case 1:return Fa(t.type,!0);case 31:return ys("Activity");default:return""}}function Il(t){try{var s="";do s+=Ud(t),t=t.return;while(t);return s}catch(i){return` -Error generating stack: `+i.message+` -`+i.stack}}function tn(t){switch(typeof t){case"bigint":case"boolean":case"number":case"string":case"undefined":return t;case"object":return t;default:return""}}function Ll(t){var s=t.type;return(t=t.nodeName)&&t.toLowerCase()==="input"&&(s==="checkbox"||s==="radio")}function Vd(t){var s=Ll(t)?"checked":"value",i=Object.getOwnPropertyDescriptor(t.constructor.prototype,s),u=""+t[s];if(!t.hasOwnProperty(s)&&typeof i<"u"&&typeof i.get=="function"&&typeof i.set=="function"){var p=i.get,v=i.set;return Object.defineProperty(t,s,{configurable:!0,get:function(){return p.call(this)},set:function(A){u=""+A,v.call(this,A)}}),Object.defineProperty(t,s,{enumerable:i.enumerable}),{getValue:function(){return u},setValue:function(A){u=""+A},stopTracking:function(){t._valueTracker=null,delete t[s]}}}}function jo(t){t._valueTracker||(t._valueTracker=Vd(t))}function Ya(t){if(!t)return!1;var s=t._valueTracker;if(!s)return!0;var i=s.getValue(),u="";return t&&(u=Ll(t)?t.checked?"true":"false":t.value),t=u,t!==i?(s.setValue(t),!0):!1}function _o(t){if(t=t||(typeof document<"u"?document:void 0),typeof t>"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var qd=/[\n"\\]/g;function nn(t){return t.replace(qd,function(s){return"\\"+s.charCodeAt(0).toString(16)+" "})}function zr(t,s,i,u,p,v,A,z){t.name="",A!=null&&typeof A!="function"&&typeof A!="symbol"&&typeof A!="boolean"?t.type=A:t.removeAttribute("type"),s!=null?A==="number"?(s===0&&t.value===""||t.value!=s)&&(t.value=""+tn(s)):t.value!==""+tn(s)&&(t.value=""+tn(s)):A!=="submit"&&A!=="reset"||t.removeAttribute("value"),s!=null?Ga(t,A,tn(s)):i!=null?Ga(t,A,tn(i)):u!=null&&t.removeAttribute("value"),p==null&&v!=null&&(t.defaultChecked=!!v),p!=null&&(t.checked=p&&typeof p!="function"&&typeof p!="symbol"),z!=null&&typeof z!="function"&&typeof z!="symbol"&&typeof z!="boolean"?t.name=""+tn(z):t.removeAttribute("name")}function Hl(t,s,i,u,p,v,A,z){if(v!=null&&typeof v!="function"&&typeof v!="symbol"&&typeof v!="boolean"&&(t.type=v),s!=null||i!=null){if(!(v!=="submit"&&v!=="reset"||s!=null))return;i=i!=null?""+tn(i):"",s=s!=null?""+tn(s):i,z||s===t.value||(t.value=s),t.defaultValue=s}u=u??p,u=typeof u!="function"&&typeof u!="symbol"&&!!u,t.checked=z?t.checked:!!u,t.defaultChecked=!!u,A!=null&&typeof A!="function"&&typeof A!="symbol"&&typeof A!="boolean"&&(t.name=A)}function Ga(t,s,i){s==="number"&&_o(t.ownerDocument)===t||t.defaultValue===""+i||(t.defaultValue=""+i)}function vs(t,s,i,u){if(t=t.options,s){s={};for(var p=0;p"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Zd=!1;if(bs)try{var Za={};Object.defineProperty(Za,"passive",{get:function(){Zd=!0}}),window.addEventListener("test",Za,Za),window.removeEventListener("test",Za,Za)}catch{Zd=!1}var Zs=null,Wd=null,Bl=null;function kg(){if(Bl)return Bl;var t,s=Wd,i=s.length,u,p="value"in Zs?Zs.value:Zs.textContent,v=p.length;for(t=0;t=Qa),Og=" ",zg=!1;function Ig(t,s){switch(t){case"keyup":return Uj.indexOf(s.keyCode)!==-1;case"keydown":return s.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Lg(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var Ao=!1;function qj(t,s){switch(t){case"compositionend":return Lg(s);case"keypress":return s.which!==32?null:(zg=!0,Og);case"textInput":return t=s.data,t===Og&&zg?null:t;default:return null}}function Fj(t,s){if(Ao)return t==="compositionend"||!tf&&Ig(t,s)?(t=kg(),Bl=Wd=Zs=null,Ao=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(s.ctrlKey||s.altKey||s.metaKey)||s.ctrlKey&&s.altKey){if(s.char&&1=s)return{node:i,offset:s-t};t=u}e:{for(;i;){if(i.nextSibling){i=i.nextSibling;break e}i=i.parentNode}i=void 0}i=Fg(i)}}function Gg(t,s){return t&&s?t===s?!0:t&&t.nodeType===3?!1:s&&s.nodeType===3?Gg(t,s.parentNode):"contains"in t?t.contains(s):t.compareDocumentPosition?!!(t.compareDocumentPosition(s)&16):!1:!1}function Xg(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var s=_o(t.document);s instanceof t.HTMLIFrameElement;){try{var i=typeof s.contentWindow.location.href=="string"}catch{i=!1}if(i)t=s.contentWindow;else break;s=_o(t.document)}return s}function rf(t){var s=t&&t.nodeName&&t.nodeName.toLowerCase();return s&&(s==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||s==="textarea"||t.contentEditable==="true")}var Jj=bs&&"documentMode"in document&&11>=document.documentMode,Mo=null,of=null,ni=null,af=!1;function Zg(t,s,i){var u=i.window===i?i.document:i.nodeType===9?i:i.ownerDocument;af||Mo==null||Mo!==_o(u)||(u=Mo,"selectionStart"in u&&rf(u)?u={start:u.selectionStart,end:u.selectionEnd}:(u=(u.ownerDocument&&u.ownerDocument.defaultView||window).getSelection(),u={anchorNode:u.anchorNode,anchorOffset:u.anchorOffset,focusNode:u.focusNode,focusOffset:u.focusOffset}),ni&&ti(ni,u)||(ni=u,u=Mc(of,"onSelect"),0>=A,p-=A,Ns=1<<32-yt(s)+p|i<v?v:8;var A=k.T,z={};k.T=z,Yf(t,!1,s,i);try{var F=p(),re=k.S;if(re!==null&&re(z,F),F!==null&&typeof F=="object"&&typeof F.then=="function"){var le=l_(F,u);xi(t,s,le,hn(t))}else xi(t,s,u,hn(t))}catch(me){xi(t,s,{then:function(){},status:"rejected",reason:me},hn())}finally{L.p=v,k.T=A}}function m_(){}function qf(t,s,i,u){if(t.tag!==5)throw Error(a(476));var p=Wx(t).queue;Zx(t,p,s,I,i===null?m_:function(){return Kx(t),i(u)})}function Wx(t){var s=t.memoizedState;if(s!==null)return s;s={memoizedState:I,baseState:I,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Es,lastRenderedState:I},next:null};var i={};return s.next={memoizedState:i,baseState:i,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Es,lastRenderedState:i},next:null},t.memoizedState=s,t=t.alternate,t!==null&&(t.memoizedState=s),s}function Kx(t){var s=Wx(t).next.queue;xi(t,s,{},hn())}function Ff(){return Zt(zi)}function Qx(){return Rt().memoizedState}function Jx(){return Rt().memoizedState}function h_(t){for(var s=t.return;s!==null;){switch(s.tag){case 24:case 3:var i=hn();t=Qs(i);var u=Js(s,t,i);u!==null&&(pn(u,s,i),di(u,s,i)),s={cache:wf()},t.payload=s;return}s=s.return}}function p_(t,s,i){var u=hn();i={lane:u,revertLane:0,action:i,hasEagerState:!1,eagerState:null,next:null},uc(t)?t0(s,i):(i=df(t,s,i,u),i!==null&&(pn(i,t,u),n0(i,s,u)))}function e0(t,s,i){var u=hn();xi(t,s,i,u)}function xi(t,s,i,u){var p={lane:u,revertLane:0,action:i,hasEagerState:!1,eagerState:null,next:null};if(uc(t))t0(s,p);else{var v=t.alternate;if(t.lanes===0&&(v===null||v.lanes===0)&&(v=s.lastRenderedReducer,v!==null))try{var A=s.lastRenderedState,z=v(A,i);if(p.hasEagerState=!0,p.eagerState=z,cn(z,A))return Gl(t,s,p,0),gt===null&&Yl(),!1}catch{}finally{}if(i=df(t,s,p,u),i!==null)return pn(i,t,u),n0(i,s,u),!0}return!1}function Yf(t,s,i,u){if(u={lane:2,revertLane:jm(),action:u,hasEagerState:!1,eagerState:null,next:null},uc(t)){if(s)throw Error(a(479))}else s=df(t,i,u,2),s!==null&&pn(s,t,2)}function uc(t){var s=t.alternate;return t===Ze||s!==null&&s===Ze}function t0(t,s){Bo=rc=!0;var i=t.pending;i===null?s.next=s:(s.next=i.next,i.next=s),t.pending=s}function n0(t,s,i){if((i&4194048)!==0){var u=s.lanes;u&=t.pendingLanes,i|=u,s.lanes=i,La(t,i)}}var dc={readContext:Zt,use:ac,useCallback:Ct,useContext:Ct,useEffect:Ct,useImperativeHandle:Ct,useLayoutEffect:Ct,useInsertionEffect:Ct,useMemo:Ct,useReducer:Ct,useRef:Ct,useState:Ct,useDebugValue:Ct,useDeferredValue:Ct,useTransition:Ct,useSyncExternalStore:Ct,useId:Ct,useHostTransitionStatus:Ct,useFormState:Ct,useActionState:Ct,useOptimistic:Ct,useMemoCache:Ct,useCacheRefresh:Ct},s0={readContext:Zt,use:ac,useCallback:function(t,s){return rn().memoizedState=[t,s===void 0?null:s],t},useContext:Zt,useEffect:Bx,useImperativeHandle:function(t,s,i){i=i!=null?i.concat([t]):null,cc(4194308,4,qx.bind(null,s,t),i)},useLayoutEffect:function(t,s){return cc(4194308,4,t,s)},useInsertionEffect:function(t,s){cc(4,2,t,s)},useMemo:function(t,s){var i=rn();s=s===void 0?null:s;var u=t();if(Gr){Nt(!0);try{t()}finally{Nt(!1)}}return i.memoizedState=[u,s],u},useReducer:function(t,s,i){var u=rn();if(i!==void 0){var p=i(s);if(Gr){Nt(!0);try{i(s)}finally{Nt(!1)}}}else p=s;return u.memoizedState=u.baseState=p,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:p},u.queue=t,t=t.dispatch=p_.bind(null,Ze,t),[u.memoizedState,t]},useRef:function(t){var s=rn();return t={current:t},s.memoizedState=t},useState:function(t){t=Bf(t);var s=t.queue,i=e0.bind(null,Ze,s);return s.dispatch=i,[t.memoizedState,i]},useDebugValue:Uf,useDeferredValue:function(t,s){var i=rn();return Vf(i,t,s)},useTransition:function(){var t=Bf(!1);return t=Zx.bind(null,Ze,t.queue,!0,!1),rn().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,s,i){var u=Ze,p=rn();if(at){if(i===void 0)throw Error(a(407));i=i()}else{if(i=s(),gt===null)throw Error(a(349));(nt&124)!==0||jx(u,s,i)}p.memoizedState=i;var v={value:i,getSnapshot:s};return p.queue=v,Bx(Ex.bind(null,u,v,t),[t]),u.flags|=2048,Uo(9,lc(),_x.bind(null,u,v,i,s),null),i},useId:function(){var t=rn(),s=gt.identifierPrefix;if(at){var i=Ss,u=Ns;i=(u&~(1<<32-yt(u)-1)).toString(32)+i,s="«"+s+"R"+i,i=oc++,0Ve?(Pt=Oe,Oe=null):Pt=Oe.sibling;var rt=oe(Q,Oe,se[Ve],ce);if(rt===null){Oe===null&&(Oe=Pt);break}t&&Oe&&rt.alternate===null&&s(Q,Oe),X=v(rt,X,Ve),Ke===null?Ce=rt:Ke.sibling=rt,Ke=rt,Oe=Pt}if(Ve===se.length)return i(Q,Oe),at&&Pr(Q,Ve),Ce;if(Oe===null){for(;VeVe?(Pt=Oe,Oe=null):Pt=Oe.sibling;var gr=oe(Q,Oe,rt.value,ce);if(gr===null){Oe===null&&(Oe=Pt);break}t&&Oe&&gr.alternate===null&&s(Q,Oe),X=v(gr,X,Ve),Ke===null?Ce=gr:Ke.sibling=gr,Ke=gr,Oe=Pt}if(rt.done)return i(Q,Oe),at&&Pr(Q,Ve),Ce;if(Oe===null){for(;!rt.done;Ve++,rt=se.next())rt=me(Q,rt.value,ce),rt!==null&&(X=v(rt,X,Ve),Ke===null?Ce=rt:Ke.sibling=rt,Ke=rt);return at&&Pr(Q,Ve),Ce}for(Oe=u(Oe);!rt.done;Ve++,rt=se.next())rt=ae(Oe,Q,Ve,rt.value,ce),rt!==null&&(t&&rt.alternate!==null&&Oe.delete(rt.key===null?Ve:rt.key),X=v(rt,X,Ve),Ke===null?Ce=rt:Ke.sibling=rt,Ke=rt);return t&&Oe.forEach(function(xE){return s(Q,xE)}),at&&Pr(Q,Ve),Ce}function mt(Q,X,se,ce){if(typeof se=="object"&&se!==null&&se.type===S&&se.key===null&&(se=se.props.children),typeof se=="object"&&se!==null){switch(se.$$typeof){case x:e:{for(var Ce=se.key;X!==null;){if(X.key===Ce){if(Ce=se.type,Ce===S){if(X.tag===7){i(Q,X.sibling),ce=p(X,se.props.children),ce.return=Q,Q=ce;break e}}else if(X.elementType===Ce||typeof Ce=="object"&&Ce!==null&&Ce.$$typeof===B&&o0(Ce)===X.type){i(Q,X.sibling),ce=p(X,se.props),vi(ce,se),ce.return=Q,Q=ce;break e}i(Q,X);break}else s(Q,X);X=X.sibling}se.type===S?(ce=$r(se.props.children,Q.mode,ce,se.key),ce.return=Q,Q=ce):(ce=Zl(se.type,se.key,se.props,null,Q.mode,ce),vi(ce,se),ce.return=Q,Q=ce)}return A(Q);case b:e:{for(Ce=se.key;X!==null;){if(X.key===Ce)if(X.tag===4&&X.stateNode.containerInfo===se.containerInfo&&X.stateNode.implementation===se.implementation){i(Q,X.sibling),ce=p(X,se.children||[]),ce.return=Q,Q=ce;break e}else{i(Q,X);break}else s(Q,X);X=X.sibling}ce=hf(se,Q.mode,ce),ce.return=Q,Q=ce}return A(Q);case B:return Ce=se._init,se=Ce(se._payload),mt(Q,X,se,ce)}if(U(se))return qe(Q,X,se,ce);if(G(se)){if(Ce=G(se),typeof Ce!="function")throw Error(a(150));return se=Ce.call(se),Be(Q,X,se,ce)}if(typeof se.then=="function")return mt(Q,X,fc(se),ce);if(se.$$typeof===E)return mt(Q,X,Jl(Q,se),ce);mc(Q,se)}return typeof se=="string"&&se!==""||typeof se=="number"||typeof se=="bigint"?(se=""+se,X!==null&&X.tag===6?(i(Q,X.sibling),ce=p(X,se),ce.return=Q,Q=ce):(i(Q,X),ce=mf(se,Q.mode,ce),ce.return=Q,Q=ce),A(Q)):i(Q,X)}return function(Q,X,se,ce){try{yi=0;var Ce=mt(Q,X,se,ce);return Vo=null,Ce}catch(Oe){if(Oe===ci||Oe===tc)throw Oe;var Ke=un(29,Oe,null,Q.mode);return Ke.lanes=ce,Ke.return=Q,Ke}finally{}}}var qo=a0(!0),i0=a0(!1),En=$(null),Wn=null;function tr(t){var s=t.alternate;V(zt,zt.current&1),V(En,t),Wn===null&&(s===null||$o.current!==null||s.memoizedState!==null)&&(Wn=t)}function l0(t){if(t.tag===22){if(V(zt,zt.current),V(En,t),Wn===null){var s=t.alternate;s!==null&&s.memoizedState!==null&&(Wn=t)}}else nr()}function nr(){V(zt,zt.current),V(En,En.current)}function Cs(t){Y(En),Wn===t&&(Wn=null),Y(zt)}var zt=$(0);function hc(t){for(var s=t;s!==null;){if(s.tag===13){var i=s.memoizedState;if(i!==null&&(i=i.dehydrated,i===null||i.data==="$?"||Im(i)))return s}else if(s.tag===19&&s.memoizedProps.revealOrder!==void 0){if((s.flags&128)!==0)return s}else if(s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break;for(;s.sibling===null;){if(s.return===null||s.return===t)return null;s=s.return}s.sibling.return=s.return,s=s.sibling}return null}function Gf(t,s,i,u){s=t.memoizedState,i=i(u,s),i=i==null?s:g({},s,i),t.memoizedState=i,t.lanes===0&&(t.updateQueue.baseState=i)}var Xf={enqueueSetState:function(t,s,i){t=t._reactInternals;var u=hn(),p=Qs(u);p.payload=s,i!=null&&(p.callback=i),s=Js(t,p,u),s!==null&&(pn(s,t,u),di(s,t,u))},enqueueReplaceState:function(t,s,i){t=t._reactInternals;var u=hn(),p=Qs(u);p.tag=1,p.payload=s,i!=null&&(p.callback=i),s=Js(t,p,u),s!==null&&(pn(s,t,u),di(s,t,u))},enqueueForceUpdate:function(t,s){t=t._reactInternals;var i=hn(),u=Qs(i);u.tag=2,s!=null&&(u.callback=s),s=Js(t,u,i),s!==null&&(pn(s,t,i),di(s,t,i))}};function c0(t,s,i,u,p,v,A){return t=t.stateNode,typeof t.shouldComponentUpdate=="function"?t.shouldComponentUpdate(u,v,A):s.prototype&&s.prototype.isPureReactComponent?!ti(i,u)||!ti(p,v):!0}function u0(t,s,i,u){t=s.state,typeof s.componentWillReceiveProps=="function"&&s.componentWillReceiveProps(i,u),typeof s.UNSAFE_componentWillReceiveProps=="function"&&s.UNSAFE_componentWillReceiveProps(i,u),s.state!==t&&Xf.enqueueReplaceState(s,s.state,null)}function Xr(t,s){var i=s;if("ref"in s){i={};for(var u in s)u!=="ref"&&(i[u]=s[u])}if(t=t.defaultProps){i===s&&(i=g({},i));for(var p in t)i[p]===void 0&&(i[p]=t[p])}return i}var pc=typeof reportError=="function"?reportError:function(t){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var s=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof t=="object"&&t!==null&&typeof t.message=="string"?String(t.message):String(t),error:t});if(!window.dispatchEvent(s))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",t);return}console.error(t)};function d0(t){pc(t)}function f0(t){console.error(t)}function m0(t){pc(t)}function gc(t,s){try{var i=t.onUncaughtError;i(s.value,{componentStack:s.stack})}catch(u){setTimeout(function(){throw u})}}function h0(t,s,i){try{var u=t.onCaughtError;u(i.value,{componentStack:i.stack,errorBoundary:s.tag===1?s.stateNode:null})}catch(p){setTimeout(function(){throw p})}}function Zf(t,s,i){return i=Qs(i),i.tag=3,i.payload={element:null},i.callback=function(){gc(t,s)},i}function p0(t){return t=Qs(t),t.tag=3,t}function g0(t,s,i,u){var p=i.type.getDerivedStateFromError;if(typeof p=="function"){var v=u.value;t.payload=function(){return p(v)},t.callback=function(){h0(s,i,u)}}var A=i.stateNode;A!==null&&typeof A.componentDidCatch=="function"&&(t.callback=function(){h0(s,i,u),typeof p!="function"&&(lr===null?lr=new Set([this]):lr.add(this));var z=u.stack;this.componentDidCatch(u.value,{componentStack:z!==null?z:""})})}function x_(t,s,i,u,p){if(i.flags|=32768,u!==null&&typeof u=="object"&&typeof u.then=="function"){if(s=i.alternate,s!==null&&ai(s,i,p,!0),i=En.current,i!==null){switch(i.tag){case 13:return Wn===null?vm():i.alternate===null&&_t===0&&(_t=3),i.flags&=-257,i.flags|=65536,i.lanes=p,u===jf?i.flags|=16384:(s=i.updateQueue,s===null?i.updateQueue=new Set([u]):s.add(u),wm(t,u,p)),!1;case 22:return i.flags|=65536,u===jf?i.flags|=16384:(s=i.updateQueue,s===null?(s={transitions:null,markerInstances:null,retryQueue:new Set([u])},i.updateQueue=s):(i=s.retryQueue,i===null?s.retryQueue=new Set([u]):i.add(u)),wm(t,u,p)),!1}throw Error(a(435,i.tag))}return wm(t,u,p),vm(),!1}if(at)return s=En.current,s!==null?((s.flags&65536)===0&&(s.flags|=256),s.flags|=65536,s.lanes=p,u!==xf&&(t=Error(a(422),{cause:u}),oi(Nn(t,i)))):(u!==xf&&(s=Error(a(423),{cause:u}),oi(Nn(s,i))),t=t.current.alternate,t.flags|=65536,p&=-p,t.lanes|=p,u=Nn(u,i),p=Zf(t.stateNode,u,p),Cf(t,p),_t!==4&&(_t=2)),!1;var v=Error(a(520),{cause:u});if(v=Nn(v,i),Ei===null?Ei=[v]:Ei.push(v),_t!==4&&(_t=2),s===null)return!0;u=Nn(u,i),i=s;do{switch(i.tag){case 3:return i.flags|=65536,t=p&-p,i.lanes|=t,t=Zf(i.stateNode,u,t),Cf(i,t),!1;case 1:if(s=i.type,v=i.stateNode,(i.flags&128)===0&&(typeof s.getDerivedStateFromError=="function"||v!==null&&typeof v.componentDidCatch=="function"&&(lr===null||!lr.has(v))))return i.flags|=65536,p&=-p,i.lanes|=p,p=p0(p),g0(p,t,i,u),Cf(i,p),!1}i=i.return}while(i!==null);return!1}var x0=Error(a(461)),$t=!1;function Vt(t,s,i,u){s.child=t===null?i0(s,null,i,u):qo(s,t.child,i,u)}function y0(t,s,i,u,p){i=i.render;var v=s.ref;if("ref"in u){var A={};for(var z in u)z!=="ref"&&(A[z]=u[z])}else A=u;return Fr(s),u=Rf(t,s,i,A,v,p),z=Df(),t!==null&&!$t?(Of(t,s,p),ks(t,s,p)):(at&&z&&pf(s),s.flags|=1,Vt(t,s,u,p),s.child)}function v0(t,s,i,u,p){if(t===null){var v=i.type;return typeof v=="function"&&!ff(v)&&v.defaultProps===void 0&&i.compare===null?(s.tag=15,s.type=v,b0(t,s,v,u,p)):(t=Zl(i.type,null,u,s,s.mode,p),t.ref=s.ref,t.return=s,s.child=t)}if(v=t.child,!sm(t,p)){var A=v.memoizedProps;if(i=i.compare,i=i!==null?i:ti,i(A,u)&&t.ref===s.ref)return ks(t,s,p)}return s.flags|=1,t=ws(v,u),t.ref=s.ref,t.return=s,s.child=t}function b0(t,s,i,u,p){if(t!==null){var v=t.memoizedProps;if(ti(v,u)&&t.ref===s.ref)if($t=!1,s.pendingProps=u=v,sm(t,p))(t.flags&131072)!==0&&($t=!0);else return s.lanes=t.lanes,ks(t,s,p)}return Wf(t,s,i,u,p)}function w0(t,s,i){var u=s.pendingProps,p=u.children,v=t!==null?t.memoizedState:null;if(u.mode==="hidden"){if((s.flags&128)!==0){if(u=v!==null?v.baseLanes|i:i,t!==null){for(p=s.child=t.child,v=0;p!==null;)v=v|p.lanes|p.childLanes,p=p.sibling;s.childLanes=v&~u}else s.childLanes=0,s.child=null;return N0(t,s,u,i)}if((i&536870912)!==0)s.memoizedState={baseLanes:0,cachePool:null},t!==null&&ec(s,v!==null?v.cachePool:null),v!==null?bx(s,v):Af(),l0(s);else return s.lanes=s.childLanes=536870912,N0(t,s,v!==null?v.baseLanes|i:i,i)}else v!==null?(ec(s,v.cachePool),bx(s,v),nr(),s.memoizedState=null):(t!==null&&ec(s,null),Af(),nr());return Vt(t,s,p,i),s.child}function N0(t,s,i,u){var p=Sf();return p=p===null?null:{parent:Ot._currentValue,pool:p},s.memoizedState={baseLanes:i,cachePool:p},t!==null&&ec(s,null),Af(),l0(s),t!==null&&ai(t,s,u,!0),null}function xc(t,s){var i=s.ref;if(i===null)t!==null&&t.ref!==null&&(s.flags|=4194816);else{if(typeof i!="function"&&typeof i!="object")throw Error(a(284));(t===null||t.ref!==i)&&(s.flags|=4194816)}}function Wf(t,s,i,u,p){return Fr(s),i=Rf(t,s,i,u,void 0,p),u=Df(),t!==null&&!$t?(Of(t,s,p),ks(t,s,p)):(at&&u&&pf(s),s.flags|=1,Vt(t,s,i,p),s.child)}function S0(t,s,i,u,p,v){return Fr(s),s.updateQueue=null,i=Nx(s,u,i,p),wx(t),u=Df(),t!==null&&!$t?(Of(t,s,v),ks(t,s,v)):(at&&u&&pf(s),s.flags|=1,Vt(t,s,i,v),s.child)}function j0(t,s,i,u,p){if(Fr(s),s.stateNode===null){var v=Oo,A=i.contextType;typeof A=="object"&&A!==null&&(v=Zt(A)),v=new i(u,v),s.memoizedState=v.state!==null&&v.state!==void 0?v.state:null,v.updater=Xf,s.stateNode=v,v._reactInternals=s,v=s.stateNode,v.props=u,v.state=s.memoizedState,v.refs={},_f(s),A=i.contextType,v.context=typeof A=="object"&&A!==null?Zt(A):Oo,v.state=s.memoizedState,A=i.getDerivedStateFromProps,typeof A=="function"&&(Gf(s,i,A,u),v.state=s.memoizedState),typeof i.getDerivedStateFromProps=="function"||typeof v.getSnapshotBeforeUpdate=="function"||typeof v.UNSAFE_componentWillMount!="function"&&typeof v.componentWillMount!="function"||(A=v.state,typeof v.componentWillMount=="function"&&v.componentWillMount(),typeof v.UNSAFE_componentWillMount=="function"&&v.UNSAFE_componentWillMount(),A!==v.state&&Xf.enqueueReplaceState(v,v.state,null),mi(s,u,v,p),fi(),v.state=s.memoizedState),typeof v.componentDidMount=="function"&&(s.flags|=4194308),u=!0}else if(t===null){v=s.stateNode;var z=s.memoizedProps,F=Xr(i,z);v.props=F;var re=v.context,le=i.contextType;A=Oo,typeof le=="object"&&le!==null&&(A=Zt(le));var me=i.getDerivedStateFromProps;le=typeof me=="function"||typeof v.getSnapshotBeforeUpdate=="function",z=s.pendingProps!==z,le||typeof v.UNSAFE_componentWillReceiveProps!="function"&&typeof v.componentWillReceiveProps!="function"||(z||re!==A)&&u0(s,v,u,A),Ks=!1;var oe=s.memoizedState;v.state=oe,mi(s,u,v,p),fi(),re=s.memoizedState,z||oe!==re||Ks?(typeof me=="function"&&(Gf(s,i,me,u),re=s.memoizedState),(F=Ks||c0(s,i,F,u,oe,re,A))?(le||typeof v.UNSAFE_componentWillMount!="function"&&typeof v.componentWillMount!="function"||(typeof v.componentWillMount=="function"&&v.componentWillMount(),typeof v.UNSAFE_componentWillMount=="function"&&v.UNSAFE_componentWillMount()),typeof v.componentDidMount=="function"&&(s.flags|=4194308)):(typeof v.componentDidMount=="function"&&(s.flags|=4194308),s.memoizedProps=u,s.memoizedState=re),v.props=u,v.state=re,v.context=A,u=F):(typeof v.componentDidMount=="function"&&(s.flags|=4194308),u=!1)}else{v=s.stateNode,Ef(t,s),A=s.memoizedProps,le=Xr(i,A),v.props=le,me=s.pendingProps,oe=v.context,re=i.contextType,F=Oo,typeof re=="object"&&re!==null&&(F=Zt(re)),z=i.getDerivedStateFromProps,(re=typeof z=="function"||typeof v.getSnapshotBeforeUpdate=="function")||typeof v.UNSAFE_componentWillReceiveProps!="function"&&typeof v.componentWillReceiveProps!="function"||(A!==me||oe!==F)&&u0(s,v,u,F),Ks=!1,oe=s.memoizedState,v.state=oe,mi(s,u,v,p),fi();var ae=s.memoizedState;A!==me||oe!==ae||Ks||t!==null&&t.dependencies!==null&&Ql(t.dependencies)?(typeof z=="function"&&(Gf(s,i,z,u),ae=s.memoizedState),(le=Ks||c0(s,i,le,u,oe,ae,F)||t!==null&&t.dependencies!==null&&Ql(t.dependencies))?(re||typeof v.UNSAFE_componentWillUpdate!="function"&&typeof v.componentWillUpdate!="function"||(typeof v.componentWillUpdate=="function"&&v.componentWillUpdate(u,ae,F),typeof v.UNSAFE_componentWillUpdate=="function"&&v.UNSAFE_componentWillUpdate(u,ae,F)),typeof v.componentDidUpdate=="function"&&(s.flags|=4),typeof v.getSnapshotBeforeUpdate=="function"&&(s.flags|=1024)):(typeof v.componentDidUpdate!="function"||A===t.memoizedProps&&oe===t.memoizedState||(s.flags|=4),typeof v.getSnapshotBeforeUpdate!="function"||A===t.memoizedProps&&oe===t.memoizedState||(s.flags|=1024),s.memoizedProps=u,s.memoizedState=ae),v.props=u,v.state=ae,v.context=F,u=le):(typeof v.componentDidUpdate!="function"||A===t.memoizedProps&&oe===t.memoizedState||(s.flags|=4),typeof v.getSnapshotBeforeUpdate!="function"||A===t.memoizedProps&&oe===t.memoizedState||(s.flags|=1024),u=!1)}return v=u,xc(t,s),u=(s.flags&128)!==0,v||u?(v=s.stateNode,i=u&&typeof i.getDerivedStateFromError!="function"?null:v.render(),s.flags|=1,t!==null&&u?(s.child=qo(s,t.child,null,p),s.child=qo(s,null,i,p)):Vt(t,s,i,p),s.memoizedState=v.state,t=s.child):t=ks(t,s,p),t}function _0(t,s,i,u){return ri(),s.flags|=256,Vt(t,s,i,u),s.child}var Kf={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function Qf(t){return{baseLanes:t,cachePool:fx()}}function Jf(t,s,i){return t=t!==null?t.childLanes&~i:0,s&&(t|=Cn),t}function E0(t,s,i){var u=s.pendingProps,p=!1,v=(s.flags&128)!==0,A;if((A=v)||(A=t!==null&&t.memoizedState===null?!1:(zt.current&2)!==0),A&&(p=!0,s.flags&=-129),A=(s.flags&32)!==0,s.flags&=-33,t===null){if(at){if(p?tr(s):nr(),at){var z=jt,F;if(F=z){e:{for(F=z,z=Zn;F.nodeType!==8;){if(!z){z=null;break e}if(F=Hn(F.nextSibling),F===null){z=null;break e}}z=F}z!==null?(s.memoizedState={dehydrated:z,treeContext:Br!==null?{id:Ns,overflow:Ss}:null,retryLane:536870912,hydrationErrors:null},F=un(18,null,null,0),F.stateNode=z,F.return=s,s.child=F,Kt=s,jt=null,F=!0):F=!1}F||Vr(s)}if(z=s.memoizedState,z!==null&&(z=z.dehydrated,z!==null))return Im(z)?s.lanes=32:s.lanes=536870912,null;Cs(s)}return z=u.children,u=u.fallback,p?(nr(),p=s.mode,z=yc({mode:"hidden",children:z},p),u=$r(u,p,i,null),z.return=s,u.return=s,z.sibling=u,s.child=z,p=s.child,p.memoizedState=Qf(i),p.childLanes=Jf(t,A,i),s.memoizedState=Kf,u):(tr(s),em(s,z))}if(F=t.memoizedState,F!==null&&(z=F.dehydrated,z!==null)){if(v)s.flags&256?(tr(s),s.flags&=-257,s=tm(t,s,i)):s.memoizedState!==null?(nr(),s.child=t.child,s.flags|=128,s=null):(nr(),p=u.fallback,z=s.mode,u=yc({mode:"visible",children:u.children},z),p=$r(p,z,i,null),p.flags|=2,u.return=s,p.return=s,u.sibling=p,s.child=u,qo(s,t.child,null,i),u=s.child,u.memoizedState=Qf(i),u.childLanes=Jf(t,A,i),s.memoizedState=Kf,s=p);else if(tr(s),Im(z)){if(A=z.nextSibling&&z.nextSibling.dataset,A)var re=A.dgst;A=re,u=Error(a(419)),u.stack="",u.digest=A,oi({value:u,source:null,stack:null}),s=tm(t,s,i)}else if($t||ai(t,s,i,!1),A=(i&t.childLanes)!==0,$t||A){if(A=gt,A!==null&&(u=i&-i,u=(u&42)!==0?1:Ha(u),u=(u&(A.suspendedLanes|i))!==0?0:u,u!==0&&u!==F.retryLane))throw F.retryLane=u,Do(t,u),pn(A,t,u),x0;z.data==="$?"||vm(),s=tm(t,s,i)}else z.data==="$?"?(s.flags|=192,s.child=t.child,s=null):(t=F.treeContext,jt=Hn(z.nextSibling),Kt=s,at=!0,Ur=null,Zn=!1,t!==null&&(jn[_n++]=Ns,jn[_n++]=Ss,jn[_n++]=Br,Ns=t.id,Ss=t.overflow,Br=s),s=em(s,u.children),s.flags|=4096);return s}return p?(nr(),p=u.fallback,z=s.mode,F=t.child,re=F.sibling,u=ws(F,{mode:"hidden",children:u.children}),u.subtreeFlags=F.subtreeFlags&65011712,re!==null?p=ws(re,p):(p=$r(p,z,i,null),p.flags|=2),p.return=s,u.return=s,u.sibling=p,s.child=u,u=p,p=s.child,z=t.child.memoizedState,z===null?z=Qf(i):(F=z.cachePool,F!==null?(re=Ot._currentValue,F=F.parent!==re?{parent:re,pool:re}:F):F=fx(),z={baseLanes:z.baseLanes|i,cachePool:F}),p.memoizedState=z,p.childLanes=Jf(t,A,i),s.memoizedState=Kf,u):(tr(s),i=t.child,t=i.sibling,i=ws(i,{mode:"visible",children:u.children}),i.return=s,i.sibling=null,t!==null&&(A=s.deletions,A===null?(s.deletions=[t],s.flags|=16):A.push(t)),s.child=i,s.memoizedState=null,i)}function em(t,s){return s=yc({mode:"visible",children:s},t.mode),s.return=t,t.child=s}function yc(t,s){return t=un(22,t,null,s),t.lanes=0,t.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null},t}function tm(t,s,i){return qo(s,t.child,null,i),t=em(s,s.pendingProps.children),t.flags|=2,s.memoizedState=null,t}function C0(t,s,i){t.lanes|=s;var u=t.alternate;u!==null&&(u.lanes|=s),vf(t.return,s,i)}function nm(t,s,i,u,p){var v=t.memoizedState;v===null?t.memoizedState={isBackwards:s,rendering:null,renderingStartTime:0,last:u,tail:i,tailMode:p}:(v.isBackwards=s,v.rendering=null,v.renderingStartTime=0,v.last=u,v.tail=i,v.tailMode=p)}function k0(t,s,i){var u=s.pendingProps,p=u.revealOrder,v=u.tail;if(Vt(t,s,u.children,i),u=zt.current,(u&2)!==0)u=u&1|2,s.flags|=128;else{if(t!==null&&(t.flags&128)!==0)e:for(t=s.child;t!==null;){if(t.tag===13)t.memoizedState!==null&&C0(t,i,s);else if(t.tag===19)C0(t,i,s);else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===s)break e;for(;t.sibling===null;){if(t.return===null||t.return===s)break e;t=t.return}t.sibling.return=t.return,t=t.sibling}u&=1}switch(V(zt,u),p){case"forwards":for(i=s.child,p=null;i!==null;)t=i.alternate,t!==null&&hc(t)===null&&(p=i),i=i.sibling;i=p,i===null?(p=s.child,s.child=null):(p=i.sibling,i.sibling=null),nm(s,!1,p,i,v);break;case"backwards":for(i=null,p=s.child,s.child=null;p!==null;){if(t=p.alternate,t!==null&&hc(t)===null){s.child=p;break}t=p.sibling,p.sibling=i,i=p,p=t}nm(s,!0,i,null,v);break;case"together":nm(s,!1,null,null,void 0);break;default:s.memoizedState=null}return s.child}function ks(t,s,i){if(t!==null&&(s.dependencies=t.dependencies),ir|=s.lanes,(i&s.childLanes)===0)if(t!==null){if(ai(t,s,i,!1),(i&s.childLanes)===0)return null}else return null;if(t!==null&&s.child!==t.child)throw Error(a(153));if(s.child!==null){for(t=s.child,i=ws(t,t.pendingProps),s.child=i,i.return=s;t.sibling!==null;)t=t.sibling,i=i.sibling=ws(t,t.pendingProps),i.return=s;i.sibling=null}return s.child}function sm(t,s){return(t.lanes&s)!==0?!0:(t=t.dependencies,!!(t!==null&&Ql(t)))}function y_(t,s,i){switch(s.tag){case 3:ie(s,s.stateNode.containerInfo),Ws(s,Ot,t.memoizedState.cache),ri();break;case 27:case 5:be(s);break;case 4:ie(s,s.stateNode.containerInfo);break;case 10:Ws(s,s.type,s.memoizedProps.value);break;case 13:var u=s.memoizedState;if(u!==null)return u.dehydrated!==null?(tr(s),s.flags|=128,null):(i&s.child.childLanes)!==0?E0(t,s,i):(tr(s),t=ks(t,s,i),t!==null?t.sibling:null);tr(s);break;case 19:var p=(t.flags&128)!==0;if(u=(i&s.childLanes)!==0,u||(ai(t,s,i,!1),u=(i&s.childLanes)!==0),p){if(u)return k0(t,s,i);s.flags|=128}if(p=s.memoizedState,p!==null&&(p.rendering=null,p.tail=null,p.lastEffect=null),V(zt,zt.current),u)break;return null;case 22:case 23:return s.lanes=0,w0(t,s,i);case 24:Ws(s,Ot,t.memoizedState.cache)}return ks(t,s,i)}function A0(t,s,i){if(t!==null)if(t.memoizedProps!==s.pendingProps)$t=!0;else{if(!sm(t,i)&&(s.flags&128)===0)return $t=!1,y_(t,s,i);$t=(t.flags&131072)!==0}else $t=!1,at&&(s.flags&1048576)!==0&&ox(s,Kl,s.index);switch(s.lanes=0,s.tag){case 16:e:{t=s.pendingProps;var u=s.elementType,p=u._init;if(u=p(u._payload),s.type=u,typeof u=="function")ff(u)?(t=Xr(u,t),s.tag=1,s=j0(null,s,u,t,i)):(s.tag=0,s=Wf(null,s,u,t,i));else{if(u!=null){if(p=u.$$typeof,p===T){s.tag=11,s=y0(null,s,u,t,i);break e}else if(p===O){s.tag=14,s=v0(null,s,u,t,i);break e}}throw s=P(u)||u,Error(a(306,s,""))}}return s;case 0:return Wf(t,s,s.type,s.pendingProps,i);case 1:return u=s.type,p=Xr(u,s.pendingProps),j0(t,s,u,p,i);case 3:e:{if(ie(s,s.stateNode.containerInfo),t===null)throw Error(a(387));u=s.pendingProps;var v=s.memoizedState;p=v.element,Ef(t,s),mi(s,u,null,i);var A=s.memoizedState;if(u=A.cache,Ws(s,Ot,u),u!==v.cache&&bf(s,[Ot],i,!0),fi(),u=A.element,v.isDehydrated)if(v={element:u,isDehydrated:!1,cache:A.cache},s.updateQueue.baseState=v,s.memoizedState=v,s.flags&256){s=_0(t,s,u,i);break e}else if(u!==p){p=Nn(Error(a(424)),s),oi(p),s=_0(t,s,u,i);break e}else{switch(t=s.stateNode.containerInfo,t.nodeType){case 9:t=t.body;break;default:t=t.nodeName==="HTML"?t.ownerDocument.body:t}for(jt=Hn(t.firstChild),Kt=s,at=!0,Ur=null,Zn=!0,i=i0(s,null,u,i),s.child=i;i;)i.flags=i.flags&-3|4096,i=i.sibling}else{if(ri(),u===p){s=ks(t,s,i);break e}Vt(t,s,u,i)}s=s.child}return s;case 26:return xc(t,s),t===null?(i=Dy(s.type,null,s.pendingProps,null))?s.memoizedState=i:at||(i=s.type,t=s.pendingProps,u=Rc(ue.current).createElement(i),u[Ht]=s,u[Xt]=t,Ft(u,i,t),Mt(u),s.stateNode=u):s.memoizedState=Dy(s.type,t.memoizedProps,s.pendingProps,t.memoizedState),null;case 27:return be(s),t===null&&at&&(u=s.stateNode=My(s.type,s.pendingProps,ue.current),Kt=s,Zn=!0,p=jt,dr(s.type)?(Lm=p,jt=Hn(u.firstChild)):jt=p),Vt(t,s,s.pendingProps.children,i),xc(t,s),t===null&&(s.flags|=4194304),s.child;case 5:return t===null&&at&&((p=u=jt)&&(u=Y_(u,s.type,s.pendingProps,Zn),u!==null?(s.stateNode=u,Kt=s,jt=Hn(u.firstChild),Zn=!1,p=!0):p=!1),p||Vr(s)),be(s),p=s.type,v=s.pendingProps,A=t!==null?t.memoizedProps:null,u=v.children,Dm(p,v)?u=null:A!==null&&Dm(p,A)&&(s.flags|=32),s.memoizedState!==null&&(p=Rf(t,s,u_,null,null,i),zi._currentValue=p),xc(t,s),Vt(t,s,u,i),s.child;case 6:return t===null&&at&&((t=i=jt)&&(i=G_(i,s.pendingProps,Zn),i!==null?(s.stateNode=i,Kt=s,jt=null,t=!0):t=!1),t||Vr(s)),null;case 13:return E0(t,s,i);case 4:return ie(s,s.stateNode.containerInfo),u=s.pendingProps,t===null?s.child=qo(s,null,u,i):Vt(t,s,u,i),s.child;case 11:return y0(t,s,s.type,s.pendingProps,i);case 7:return Vt(t,s,s.pendingProps,i),s.child;case 8:return Vt(t,s,s.pendingProps.children,i),s.child;case 12:return Vt(t,s,s.pendingProps.children,i),s.child;case 10:return u=s.pendingProps,Ws(s,s.type,u.value),Vt(t,s,u.children,i),s.child;case 9:return p=s.type._context,u=s.pendingProps.children,Fr(s),p=Zt(p),u=u(p),s.flags|=1,Vt(t,s,u,i),s.child;case 14:return v0(t,s,s.type,s.pendingProps,i);case 15:return b0(t,s,s.type,s.pendingProps,i);case 19:return k0(t,s,i);case 31:return u=s.pendingProps,i=s.mode,u={mode:u.mode,children:u.children},t===null?(i=yc(u,i),i.ref=s.ref,s.child=i,i.return=s,s=i):(i=ws(t.child,u),i.ref=s.ref,s.child=i,i.return=s,s=i),s;case 22:return w0(t,s,i);case 24:return Fr(s),u=Zt(Ot),t===null?(p=Sf(),p===null&&(p=gt,v=wf(),p.pooledCache=v,v.refCount++,v!==null&&(p.pooledCacheLanes|=i),p=v),s.memoizedState={parent:u,cache:p},_f(s),Ws(s,Ot,p)):((t.lanes&i)!==0&&(Ef(t,s),mi(s,null,null,i),fi()),p=t.memoizedState,v=s.memoizedState,p.parent!==u?(p={parent:u,cache:u},s.memoizedState=p,s.lanes===0&&(s.memoizedState=s.updateQueue.baseState=p),Ws(s,Ot,u)):(u=v.cache,Ws(s,Ot,u),u!==p.cache&&bf(s,[Ot],i,!0))),Vt(t,s,s.pendingProps.children,i),s.child;case 29:throw s.pendingProps}throw Error(a(156,s.tag))}function As(t){t.flags|=4}function M0(t,s){if(s.type!=="stylesheet"||(s.state.loading&4)!==0)t.flags&=-16777217;else if(t.flags|=16777216,!Hy(s)){if(s=En.current,s!==null&&((nt&4194048)===nt?Wn!==null:(nt&62914560)!==nt&&(nt&536870912)===0||s!==Wn))throw ui=jf,mx;t.flags|=8192}}function vc(t,s){s!==null&&(t.flags|=4),t.flags&16384&&(s=t.tag!==22?ot():536870912,t.lanes|=s,Xo|=s)}function bi(t,s){if(!at)switch(t.tailMode){case"hidden":s=t.tail;for(var i=null;s!==null;)s.alternate!==null&&(i=s),s=s.sibling;i===null?t.tail=null:i.sibling=null;break;case"collapsed":i=t.tail;for(var u=null;i!==null;)i.alternate!==null&&(u=i),i=i.sibling;u===null?s||t.tail===null?t.tail=null:t.tail.sibling=null:u.sibling=null}}function St(t){var s=t.alternate!==null&&t.alternate.child===t.child,i=0,u=0;if(s)for(var p=t.child;p!==null;)i|=p.lanes|p.childLanes,u|=p.subtreeFlags&65011712,u|=p.flags&65011712,p.return=t,p=p.sibling;else for(p=t.child;p!==null;)i|=p.lanes|p.childLanes,u|=p.subtreeFlags,u|=p.flags,p.return=t,p=p.sibling;return t.subtreeFlags|=u,t.childLanes=i,s}function v_(t,s,i){var u=s.pendingProps;switch(gf(s),s.tag){case 31:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return St(s),null;case 1:return St(s),null;case 3:return i=s.stateNode,u=null,t!==null&&(u=t.memoizedState.cache),s.memoizedState.cache!==u&&(s.flags|=2048),_s(Ot),ge(),i.pendingContext&&(i.context=i.pendingContext,i.pendingContext=null),(t===null||t.child===null)&&(si(s)?As(s):t===null||t.memoizedState.isDehydrated&&(s.flags&256)===0||(s.flags|=1024,lx())),St(s),null;case 26:return i=s.memoizedState,t===null?(As(s),i!==null?(St(s),M0(s,i)):(St(s),s.flags&=-16777217)):i?i!==t.memoizedState?(As(s),St(s),M0(s,i)):(St(s),s.flags&=-16777217):(t.memoizedProps!==u&&As(s),St(s),s.flags&=-16777217),null;case 27:we(s),i=ue.current;var p=s.type;if(t!==null&&s.stateNode!=null)t.memoizedProps!==u&&As(s);else{if(!u){if(s.stateNode===null)throw Error(a(166));return St(s),null}t=W.current,si(s)?ax(s):(t=My(p,u,i),s.stateNode=t,As(s))}return St(s),null;case 5:if(we(s),i=s.type,t!==null&&s.stateNode!=null)t.memoizedProps!==u&&As(s);else{if(!u){if(s.stateNode===null)throw Error(a(166));return St(s),null}if(t=W.current,si(s))ax(s);else{switch(p=Rc(ue.current),t){case 1:t=p.createElementNS("http://www.w3.org/2000/svg",i);break;case 2:t=p.createElementNS("http://www.w3.org/1998/Math/MathML",i);break;default:switch(i){case"svg":t=p.createElementNS("http://www.w3.org/2000/svg",i);break;case"math":t=p.createElementNS("http://www.w3.org/1998/Math/MathML",i);break;case"script":t=p.createElement("div"),t.innerHTML="