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
288 lines
9.3 KiB
Python
288 lines
9.3 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
from __future__ import annotations
|
|
|
|
import functools
|
|
import logging
|
|
from contextlib import contextmanager
|
|
from typing import TYPE_CHECKING, Any, AsyncContextManager, Awaitable, Callable, ContextManager, List, Optional, TypeVar
|
|
|
|
from agentlightning.store.base import LightningStore
|
|
from agentlightning.types import Attributes, ParallelWorkerBase, Span, SpanCoreFields, SpanRecordingContext, TraceStatus
|
|
|
|
if TYPE_CHECKING:
|
|
from langchain_core.callbacks.base import BaseCallbackHandler # type: ignore
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
_active_tracer: Optional[Tracer] = None
|
|
|
|
T_func = Callable[..., Awaitable[Any]]
|
|
|
|
|
|
class Tracer(ParallelWorkerBase):
|
|
"""
|
|
An abstract base class for tracers.
|
|
|
|
This class defines a standard interface for tracing code execution,
|
|
capturing the resulting spans, and providing them for analysis. It is
|
|
designed to be backend-agnostic, allowing for different implementations
|
|
(e.g., for AgentOps, OpenTelemetry, Docker, etc.).
|
|
|
|
The primary interaction pattern is through the [`trace_context`][agentlightning.Tracer.trace_context]
|
|
context manager, which ensures that traces are properly started and captured,
|
|
even in the case of exceptions.
|
|
|
|
A typical workflow:
|
|
|
|
```python
|
|
tracer = YourTracerImplementation()
|
|
|
|
try:
|
|
async with tracer.trace_context(name="my_traced_task"):
|
|
# ... code to be traced ...
|
|
await run_my_agent_logic()
|
|
except Exception as e:
|
|
print(f"An error occurred: {e}")
|
|
|
|
# Retrieve the trace data after the context block
|
|
spans: list[ReadableSpan] = tracer.get_last_trace()
|
|
|
|
# Process the trace data
|
|
if trace_tree:
|
|
rl_triplets = TracerTraceToTriplet().adapt(spans)
|
|
# ... do something with the triplets
|
|
```
|
|
"""
|
|
|
|
_store: Optional[LightningStore] = None
|
|
|
|
def init_worker(self, worker_id: int, store: Optional[LightningStore] = None) -> None:
|
|
"""Initialize the tracer for a worker.
|
|
|
|
Args:
|
|
worker_id: The ID of the worker.
|
|
store: The store to add the spans to. If it's provided, traces will be added to the store when tracing.
|
|
"""
|
|
super().init_worker(worker_id)
|
|
self._store = store
|
|
|
|
def trace_context(
|
|
self,
|
|
name: Optional[str] = None,
|
|
*,
|
|
store: Optional[LightningStore] = None,
|
|
rollout_id: Optional[str] = None,
|
|
attempt_id: Optional[str] = None,
|
|
) -> AsyncContextManager[Any]:
|
|
"""
|
|
Starts a new tracing context. This should be used as a context manager.
|
|
|
|
The implementation should handle the setup and teardown of the tracing
|
|
for the enclosed code block. It must ensure that any spans generated
|
|
within the `with` block are collected and made available via
|
|
[`get_last_trace`][agentlightning.Tracer.get_last_trace].
|
|
|
|
Args:
|
|
name: The name for the root span of this trace context.
|
|
store: The store to add the spans to. Deprecated in favor of passing store to init_worker().
|
|
rollout_id: The rollout ID to add the spans to.
|
|
attempt_id: The attempt ID to add the spans to.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def _trace_context_sync(
|
|
self,
|
|
name: Optional[str] = None,
|
|
*,
|
|
rollout_id: Optional[str] = None,
|
|
attempt_id: Optional[str] = None,
|
|
) -> ContextManager[Any]:
|
|
"""Internal API for CI backward compatibility."""
|
|
raise NotImplementedError()
|
|
|
|
def get_last_trace(self) -> List[Span]:
|
|
"""
|
|
Retrieves the raw list of captured spans from the most recent trace.
|
|
|
|
Returns:
|
|
A list of [`Span`][agentlightning.Span] objects collected during the last trace.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def trace_run(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
|
|
"""
|
|
A convenience wrapper to trace the execution of a single synchronous function.
|
|
|
|
Deprecated in favor of customizing Runners.
|
|
|
|
Args:
|
|
func: The synchronous function to execute and trace.
|
|
*args: Positional arguments to pass to the function.
|
|
**kwargs: Keyword arguments to pass to the function.
|
|
|
|
Returns:
|
|
The return value of the function.
|
|
"""
|
|
with self._trace_context_sync(name=func.__name__):
|
|
return func(*args, **kwargs)
|
|
|
|
def create_span(
|
|
self,
|
|
name: str,
|
|
attributes: Optional[Attributes] = None,
|
|
timestamp: Optional[float] = None,
|
|
status: Optional[TraceStatus] = None,
|
|
) -> SpanCoreFields:
|
|
"""Notify the tracer that a span should be created here.
|
|
|
|
It uses a fire-and-forget approach and doesn't wait for the span to be created.
|
|
|
|
Args:
|
|
name: The name of the span.
|
|
attributes: The attributes of the span.
|
|
timestamp: The timestamp of the span.
|
|
status: The status of the span.
|
|
|
|
Returns:
|
|
The core fields of the span.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def operation_context(
|
|
self,
|
|
name: str,
|
|
attributes: Optional[Attributes] = None,
|
|
start_time: Optional[float] = None,
|
|
end_time: Optional[float] = None,
|
|
) -> ContextManager[SpanRecordingContext]:
|
|
"""Start to record an operation to a span.
|
|
|
|
Args:
|
|
name: The name of the operation.
|
|
attributes: The attributes of the operation.
|
|
start_time: The start time of the operation.
|
|
end_time: The end time of the operation.
|
|
|
|
Returns:
|
|
A [`SpanRecordingContext`][agentlightning.SpanRecordingContext] for recording the operation on the span.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
async def trace_run_async(self, func: Callable[..., Awaitable[Any]], *args: Any, **kwargs: Any) -> Any:
|
|
"""
|
|
A convenience wrapper to trace the execution of a single asynchronous function.
|
|
|
|
Deprecated in favor of customizing Runners.
|
|
|
|
Args:
|
|
func: The asynchronous function to execute and trace.
|
|
*args: Positional arguments to pass to the function.
|
|
**kwargs: Keyword arguments to pass to the function.
|
|
|
|
Returns:
|
|
The return value of the function.
|
|
"""
|
|
async with self.trace_context(name=func.__name__):
|
|
return await func(*args, **kwargs)
|
|
|
|
def get_langchain_handler(self) -> Optional[BaseCallbackHandler]: # type: ignore
|
|
"""Get a handler to install in langchain agent callback.
|
|
|
|
Agents are expected to use this handler in their agents to enable tracing.
|
|
"""
|
|
logger.warning(f"{self.__class__.__name__} does not provide a LangChain callback handler.")
|
|
return None
|
|
|
|
@contextmanager
|
|
def lifespan(self, store: Optional[LightningStore] = None):
|
|
"""A context manager to manage the lifespan of the tracer.
|
|
|
|
This can be used to set up and tear down any necessary resources
|
|
for the tracer, useful for debugging purposes.
|
|
|
|
Args:
|
|
store: The store to add the spans to. If it's provided, traces will be added to the store when tracing.
|
|
"""
|
|
has_init = False
|
|
has_init_worker = False
|
|
try:
|
|
self.init()
|
|
has_init = True
|
|
|
|
self.init_worker(0, store)
|
|
has_init_worker = True
|
|
|
|
yield
|
|
|
|
finally:
|
|
if has_init_worker:
|
|
self.teardown_worker(0)
|
|
if has_init:
|
|
self.teardown()
|
|
|
|
|
|
def set_active_tracer(tracer: Tracer):
|
|
"""Set the active tracer for the current process.
|
|
|
|
Args:
|
|
tracer: The tracer to set as active.
|
|
"""
|
|
global _active_tracer
|
|
if _active_tracer is not None:
|
|
raise ValueError("An active tracer is already set. Cannot set a new one.")
|
|
_active_tracer = tracer
|
|
|
|
|
|
def clear_active_tracer():
|
|
"""Clear the active tracer for the current process."""
|
|
global _active_tracer
|
|
_active_tracer = None
|
|
|
|
|
|
def get_active_tracer() -> Optional[Tracer]:
|
|
"""Get the active tracer for the current process.
|
|
|
|
Returns:
|
|
The active tracer, or None if no tracer is active.
|
|
"""
|
|
global _active_tracer
|
|
return _active_tracer
|
|
|
|
|
|
class _ActiveTracerAsyncCM(AsyncContextManager[T]):
|
|
def __init__(self, tracer: Tracer, inner: AsyncContextManager[T]):
|
|
self._tracer = tracer
|
|
self._inner = inner
|
|
|
|
async def __aenter__(self) -> T:
|
|
set_active_tracer(self._tracer) # will raise if nested
|
|
try:
|
|
return await self._inner.__aenter__()
|
|
except Exception:
|
|
clear_active_tracer()
|
|
raise
|
|
|
|
async def __aexit__(self, *args: Any, **kwargs: Any) -> Optional[bool]:
|
|
try:
|
|
return await self._inner.__aexit__(*args, **kwargs)
|
|
finally:
|
|
clear_active_tracer()
|
|
|
|
|
|
def with_active_tracer_context(
|
|
func: Callable[..., AsyncContextManager[T]],
|
|
) -> Callable[..., AsyncContextManager[T]]:
|
|
"""Decorate a method returning an AsyncContextManager so tracer is active for the whole `async with`."""
|
|
|
|
@functools.wraps(func)
|
|
def wrapper(self: Tracer, *args: Any, **kwargs: Any) -> AsyncContextManager[T]:
|
|
cm = func(self, *args, **kwargs)
|
|
return _ActiveTracerAsyncCM(self, cm)
|
|
|
|
return wrapper
|