Files
wehub-resource-sync 85742ab165
CPU Test / Lint - next (push) Waiting to run
Dashboard / Chromatic (push) Waiting to run
CPU Test / Lint - fast (push) Waiting to run
CPU Test / Build documentation (push) Waiting to run
CPU Test / Test (Store, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Weave, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Others, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Store, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Utilities, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Weave, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Others, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Store, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Utilities, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Weave, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Others, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Store, latest, Python 3.13) (push) Waiting to run
CPU Test / Lint - slow (push) Waiting to run
CPU Test / Lint - JavaScript (push) Waiting to run
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Others, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Utilities, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Weave, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (JavaScript) (push) Waiting to run
Deploy Documentation / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:44:17 +08:00

243 lines
11 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import logging
import os
import warnings
from contextlib import asynccontextmanager, contextmanager
from typing import TYPE_CHECKING, Any, AsyncGenerator, Iterator, List, Optional
import agentops
import agentops.sdk.core
import opentelemetry.trace as trace_api
from agentops.sdk.core import TracingCore
from opentelemetry.sdk.trace import TracerProvider as TracerProviderImpl
from opentelemetry.trace.status import StatusCode
from agentlightning.instrumentation import instrument_all, uninstrument_all
from agentlightning.store.base import LightningStore
from agentlightning.utils.otel import get_span_processors, get_tracer_provider
from .base import with_active_tracer_context
from .otel import LightningSpanProcessor, OtelTracer
if TYPE_CHECKING:
from agentops.integration.callbacks.langchain import LangchainCallbackHandler
logger = logging.getLogger(__name__)
class AgentOpsTracer(OtelTracer):
"""Traces agent execution using AgentOps.
This tracer provides functionality to capture execution details using the
AgentOps library. It manages the AgentOps client initialization, server setup,
and integration with the OpenTelemetry tracing ecosystem.
Attributes:
agentops_managed: Whether to automatically manage `agentops`.
When set to true, tracer calls `agentops.init()`
automatically and launches an agentops endpoint locally.
If not, you are responsible for calling and using it
before using the tracer.
instrument_managed: Whether to automatically manage instrumentation.
When set to false, you will manage the instrumentation
yourself and the tracer might not work as expected.
daemon: Whether the AgentOps server runs as a daemon process.
Only applicable if `agentops_managed` is True.
"""
def __init__(self, *, agentops_managed: bool = True, instrument_managed: bool = True, daemon: bool = True):
super().__init__()
self._lightning_span_processor: Optional[LightningSpanProcessor] = None
self.agentops_managed = agentops_managed
self.instrument_managed = instrument_managed
self.daemon = daemon
if not self.agentops_managed:
logger.warning("agentops_managed=False. You are responsible for AgentOps setup.")
if not self.instrument_managed:
logger.warning("instrument_managed=False. You are responsible for all instrumentation.")
def instrument(self, worker_id: int):
instrument_all()
def uninstrument(self, worker_id: int):
uninstrument_all()
def _initialize_tracer_provider(self, worker_id: int):
logger.info(f"[Worker {worker_id}] Setting up AgentOps tracer...") # worker_id included in process name
if self.instrument_managed:
self.instrument(worker_id)
logger.info(f"[Worker {worker_id}] Instrumentation applied.")
if self.agentops_managed:
os.environ.setdefault("AGENTOPS_API_KEY", "dummy")
if not agentops.get_client().initialized:
agentops.init(auto_start_session=False) # type: ignore
logger.info(f"[Worker {worker_id}] AgentOps client initialized.")
else:
logger.warning(f"[Worker {worker_id}] AgentOps client was already initialized. Skip initialization.")
span_processors = get_span_processors(self._get_tracer_provider(), LightningSpanProcessor)
if len(span_processors) > 0:
logger.warning(
"LightningSpanProcessor already present in TracerProvider. You might have called init_worker() multiple times."
"Agent-lightning will try to reuse the existing LightningSpanProcessor."
)
if len(span_processors) > 1:
logger.error("More than one LightningSpanProcessors present in TracerProvider. This should not happen.")
self._lightning_span_processor = span_processors[0]
else:
self._lightning_span_processor = LightningSpanProcessor()
self._get_tracer_provider().add_span_processor(self._lightning_span_processor) # type: ignore
def teardown_worker(self, worker_id: int) -> None:
super().teardown_worker(worker_id)
if self.instrument_managed:
self.uninstrument(worker_id)
logger.info(f"[Worker {worker_id}] Instrumentation removed.")
# NOTE: The teardown doesn't try to remove the LightningSpanProcessor from the TracerProvider.
# Currently there is no stable way to fully restore the AgentOps state to the initial state.
@with_active_tracer_context
@asynccontextmanager
async def trace_context(
self,
name: Optional[str] = None,
*,
store: Optional[LightningStore] = None,
rollout_id: Optional[str] = None,
attempt_id: Optional[str] = None,
) -> AsyncGenerator[trace_api.Tracer, None]:
"""
Starts a new tracing context. This should be used as a context manager.
Args:
name: Optional name for the tracing context.
store: Optional store to add the spans to.
rollout_id: Optional rollout ID to add the spans to.
attempt_id: Optional attempt ID to add the spans to.
Yields:
The OpenTelemetry tracer instance to collect spans.
"""
if store is not None:
warnings.warn(
"store is deprecated in favor of init_worker(). It will be removed in the future.",
DeprecationWarning,
stacklevel=3,
)
else:
store = self._store
with self._trace_context_sync(name=name, store=store, rollout_id=rollout_id, attempt_id=attempt_id) as tracer:
yield tracer
@contextmanager
def _trace_context_sync(
self,
name: Optional[str] = None,
*,
store: Optional[LightningStore] = None,
rollout_id: Optional[str] = None,
attempt_id: Optional[str] = None,
) -> Iterator[trace_api.Tracer]:
"""Implementation of `trace_context` for synchronous execution."""
if not self._lightning_span_processor:
raise RuntimeError("LightningSpanProcessor is not initialized. Call init_worker() first.")
tracer_provider = self._get_tracer_provider()
kwargs: dict[str, Any] = {}
if name is not None:
kwargs["trace_name"] = name
elif rollout_id is not None:
kwargs["trace_name"] = rollout_id
if store is not None and rollout_id is not None and attempt_id is not None:
if store.capabilities.get("otlp_traces", False) is True:
logger.debug(f"Tracing to LightningStore rollout_id={rollout_id}, attempt_id={attempt_id}")
self._enable_native_otlp_exporter(store, rollout_id, attempt_id)
else:
self._disable_native_otlp_exporter()
ctx = self._lightning_span_processor.with_context(store=store, rollout_id=rollout_id, attempt_id=attempt_id)
with ctx:
# AgentOps end_trace and start_trace must live inside the lightning span processor context.
# Otherwise some traces might not be recorded.
with self._agentops_trace_context(rollout_id, attempt_id, kwargs):
yield trace_api.get_tracer(__name__, tracer_provider=tracer_provider)
elif store is None and rollout_id is None and attempt_id is None:
self._disable_native_otlp_exporter()
with self._lightning_span_processor:
with self._agentops_trace_context(None, None, kwargs):
yield trace_api.get_tracer(__name__, tracer_provider=tracer_provider)
else:
raise ValueError("store, rollout_id, and attempt_id must be either all provided or all None")
@contextmanager
def _agentops_trace_context(self, rollout_id: Optional[str], attempt_id: Optional[str], kwargs: dict[str, Any]):
trace = agentops.start_trace(**kwargs)
status = StatusCode.OK # type: ignore
try:
yield
except Exception as e:
# This will catch errors in user code.
status = StatusCode.ERROR # type: ignore
logger.error(f"Trace failed for rollout_id={rollout_id}, attempt_id={attempt_id}: {e}")
raise # should reraise the error here so that runner can handle it
finally:
agentops.end_trace(trace, end_state=status) # type: ignore
def get_langchain_handler(self, tags: List[str] | None = None) -> LangchainCallbackHandler:
"""
Get the Langchain callback handler for integrating with Langchain.
Args:
tags: Optional list of tags to apply to the Langchain callback handler.
Returns:
An instance of the Langchain callback handler.
"""
import agentops
from agentops.integration.callbacks.langchain import LangchainCallbackHandler
tags = tags or []
client_instance = agentops.get_client()
api_key = None
if client_instance.initialized:
api_key = client_instance.config.api_key
else:
logger.warning(
"AgentOps client not initialized when creating LangchainCallbackHandler. API key may be missing."
)
return LangchainCallbackHandler(api_key=api_key, tags=tags)
get_langchain_callback_handler = get_langchain_handler # alias
def _get_tracer_provider(self) -> TracerProviderImpl:
try:
# new versions
instance = agentops.sdk.core.tracer
if instance.provider is None:
raise RuntimeError("AgentOps TracerProvider is not initialized.")
if get_tracer_provider() is not instance.provider:
logger.error(
"Mismatch between global singleton TracerProvider and AgentOps TracerProvider. "
"AgentOps might not work properly."
)
if not isinstance(instance.provider, TracerProviderImpl): # type: ignore
raise RuntimeError("Unsupported TracerProvider type for AgentOps instrumentation.")
self._tracer_provider = instance.provider
return self._tracer_provider
except AttributeError:
# old versions
instance = TracingCore.get_instance() # type: ignore
self._tracer_provider = instance._provider # type: ignore
return self._tracer_provider # type: ignore