chore: import upstream snapshot with attribution
Deploy Documentation / deploy (push) Has been cancelled
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, latest, Python 3.13) (push) Has been cancelled
Dashboard / Chromatic (push) Has been cancelled
CPU Test / Lint - fast (push) Has been cancelled
CPU Test / Lint - next (push) Has been cancelled
CPU Test / Lint - slow (push) Has been cancelled
CPU Test / Lint - JavaScript (push) Has been cancelled
CPU Test / Build documentation (push) Has been cancelled
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Others, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Store, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Weave, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Others, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Store, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Utilities, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (JavaScript) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:44:17 +08:00
commit 85742ab165
588 changed files with 320176 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
from .core import *
from .resources import *
from .tracer import *
+553
View File
@@ -0,0 +1,553 @@
# Copyright (c) Microsoft. All rights reserved.
"""Core data models shared across Agent Lightning components."""
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Generic,
Iterator,
List,
Literal,
Mapping,
Optional,
Protocol,
Sequence,
SupportsIndex,
TypedDict,
TypeVar,
Union,
cast,
overload,
)
from opentelemetry.sdk.trace import ReadableSpan
from pydantic import BaseModel, Field, model_validator
from .tracer import Span, SpanCoreFields
if TYPE_CHECKING:
from agentlightning.litagent import LitAgent
from agentlightning.runner.base import Runner
from agentlightning.tracer.base import Tracer
__all__ = [
"Triplet",
"RolloutLegacy",
"Task",
"TaskInput",
"TaskIfAny",
"RolloutRawResultLegacy",
"RolloutRawResult",
"RolloutMode",
"GenericResponse",
"ParallelWorkerBase",
"Dataset",
"AttemptStatus",
"RolloutStatus",
"RolloutConfig",
"Rollout",
"Attempt",
"AttemptedRollout",
"EnqueueRolloutRequest",
"Hook",
"Worker",
"WorkerStatus",
"PaginatedResult",
"FilterOptions",
"SortOptions",
"FilterField",
]
T_co = TypeVar("T_co", covariant=True)
class Triplet(BaseModel):
"""Single interaction turn captured during reinforcement learning."""
prompt: Any
response: Any
reward: Optional[float] = None
metadata: Dict[str, Any] = Field(default_factory=dict)
class RolloutLegacy(BaseModel):
"""Legacy reporting payload exchanged with the deprecated HTTP server.
!!! warning "Deprecated"
Use [`Rollout`][agentlightning.Rollout] instead.
"""
rollout_id: str
# Echoing the input task
task: Optional[Task] = None
# Primary, high-level feedback
final_reward: Optional[float] = None
# Structured, sequential feedback for RL-style optimization
triplets: Optional[List[Triplet]] = None
# Optional, rich-context data for deep analysis
trace: Optional[List[Dict[str, Any]]] = Field(
default=None,
description="A list of spans that conform to the OpenTelemetry JSON format. "
"Users of the opentelemetry-sdk can generate this by calling "
"json.loads(readable_span.to_json()).",
)
logs: Optional[List[str]] = None
# A bucket for any other relevant information
metadata: Dict[str, Any] = Field(default_factory=dict)
RolloutStatus = Literal[
"queuing", # initial status
"preparing", # after the trace is claimed
"running", # after receiving the first trace
"failed", # crashed
"succeeded", # status OK
"cancelled", # cancelled by user (or watchdog)
"requeuing", # retrying
]
"""The status of a rollout."""
AttemptStatus = Literal[
# A status is essentially a process.
# It should not have scheduling/management statuses like "queuing" or "cancelled".
"preparing",
"running",
"failed",
"succeeded",
"unresponsive", # the worker has not reported results for a while
"timeout", # the worker has been emitting new logs, but have been working on the task for too long
]
"""The status of an attempt."""
RolloutMode = Literal["train", "val", "test"]
"""Possible rollout modes."""
class Attempt(BaseModel):
"""Execution attempt for a rollout, including metadata for retries."""
rollout_id: str
"""The rollout which this attempt belongs to."""
attempt_id: str
"""The universal id for current attempt."""
sequence_id: int
"""The sequence number of the attempt, starting from 1."""
start_time: float
"""The time when the attempt has started."""
end_time: Optional[float] = None
"""The time when the attempt has ended."""
status: AttemptStatus = "preparing"
"""The status of the attempt."""
worker_id: Optional[str] = None
"""The rollout worker which is executing this attempt."""
last_heartbeat_time: Optional[float] = None
"""The last time when the worker has reported progress (i.e., a span)."""
metadata: Optional[Dict[str, Any]] = None
"""A bucket for any other relevant information."""
class RolloutConfig(BaseModel):
"""Configuration controlling rollout retries and timeouts."""
timeout_seconds: Optional[float] = None
"""The timeout for the rollout, in seconds. None indicates no timeout."""
unresponsive_seconds: Optional[float] = None
"""The unresponsive timeout for the rollout, in seconds. None indicates no unresponsive timeout."""
max_attempts: int = Field(default=1, ge=1)
"""The maximum number of attempts for the rollout, including the first attempt."""
retry_condition: List[AttemptStatus] = Field(default_factory=cast(Callable[[], List[AttemptStatus]], list))
"""The list of statuses that should trigger a retry."""
class Rollout(BaseModel):
rollout_id: str
"""Unique identifier for the rollout."""
input: TaskInput
"""Task input used to generate the rollout."""
# Time to track the lifecycle of the rollout
start_time: float
"""Timestamp when the rollout started."""
end_time: Optional[float] = None
"""Timestamp when the rollout ended."""
mode: Optional[RolloutMode] = None
"""Execution mode such as `"train"`, `"val"` or `"test"`. See [`RolloutMode`][agentlightning.RolloutMode]."""
resources_id: Optional[str] = None
"""Identifier of the resources required to execute the rollout."""
status: RolloutStatus = "queuing"
"""Latest status emitted by the controller."""
config: RolloutConfig = Field(default_factory=RolloutConfig)
"""Retry and timeout configuration associated with the rollout."""
metadata: Optional[Dict[str, Any]] = None
"""Additional metadata attached to the rollout."""
class AttemptedRollout(Rollout):
"""Rollout paired with the currently active attempt."""
attempt: Attempt
"""The attempt that is currently processing the rollout."""
@model_validator(mode="after")
def check_consistency(self) -> AttemptedRollout:
if self.attempt.rollout_id != self.rollout_id:
raise ValueError("Inconsistent rollout_id between Rollout and Attempt")
return self
class EnqueueRolloutRequest(BaseModel):
"""Payload describing a rollout to be queued via [`enqueue_rollout`][agentlightning.LightningStore.enqueue_rollout].
A subset of fields from [`Rollout`][agentlightning.Rollout] used for queuing new rollouts.
"""
input: TaskInput
"""Task input used to generate the rollout."""
mode: Optional[RolloutMode] = None
"""Execution mode such as `"train"`, `"val"` or `"test"`. See [`RolloutMode`][agentlightning.RolloutMode]."""
resources_id: Optional[str] = None
"""Identifier of the resources required to execute the rollout."""
config: Optional[RolloutConfig] = None
"""Retry and timeout configuration associated with the rollout."""
metadata: Optional[Dict[str, Any]] = None
"""Additional metadata attached to the rollout."""
WorkerStatus = Literal["idle", "busy", "unknown"]
class Worker(BaseModel):
"""Worker information. This is actually the same as Runner info."""
worker_id: str
"""The ID of the worker."""
status: WorkerStatus = "unknown"
"""The status of the worker."""
heartbeat_stats: Optional[Dict[str, Any]] = None
"""Statistics about the worker's heartbeat."""
last_heartbeat_time: Optional[float] = None
"""The last time when the worker has reported the stats."""
last_dequeue_time: Optional[float] = None
"""The last time when the worker has tried to dequeue a rollout."""
last_busy_time: Optional[float] = None
"""The last time when the worker has started an attempt and became busy."""
last_idle_time: Optional[float] = None
"""The last time when the worker has triggered the end of an attempt and became idle."""
current_rollout_id: Optional[str] = None
"""The ID of the current rollout that the worker is processing."""
current_attempt_id: Optional[str] = None
"""The ID of the current attempt that the worker is processing."""
TaskInput = Any
"""Task input type. Accepts arbitrary payloads."""
class Task(BaseModel):
"""Rollout request served to client agents.
!!! warning "Deprecated"
The legacy HTTP client/server stack still uses this model. Prefer
[`LightningStore`][agentlightning.LightningStore] APIs for new workflows.
"""
rollout_id: str
input: TaskInput
mode: Optional[RolloutMode] = None
resources_id: Optional[str] = None
# Optional fields for tracking task lifecycle
create_time: Optional[float] = None
last_claim_time: Optional[float] = None
num_claims: Optional[int] = None
# Allow additional metadata fields
metadata: Dict[str, Any] = Field(default_factory=dict)
class TaskIfAny(BaseModel):
"""A task or indication that no task is available.
!!! warning "Deprecated"
Use [`LightningStore`][agentlightning.LightningStore] APIs for new workflows.
"""
is_available: bool
"""Indication that a task is available."""
task: Optional[Task] = None
RolloutRawResultLegacy = Union[None, float, List[Triplet], List[Dict[str, Any]], List[ReadableSpan], RolloutLegacy]
"""Legacy rollout result type.
!!! warning "Deprecated"
Use [`RolloutRawResult`][agentlightning.RolloutRawResult] instead.
"""
RolloutRawResult = Union[
None, # nothing (relies on tracer)
float, # only final reward
List[ReadableSpan], # constructed OTEL spans by user
List[Span], # constructed Span objects by user
List[SpanCoreFields], # constructed SpanCoreFields objects by user
]
"""Rollout result type.
Possible return values of [`rollout`][agentlightning.LitAgent.rollout].
"""
class GenericResponse(BaseModel):
"""Generic server response used by compatibility endpoints.
!!! warning "Deprecated"
This response is no longer used by the new
[`LightningStore`][agentlightning.LightningStore] APIs.
Attributes:
status: Status string describing the result of the request.
message: Optional human readable explanation.
data: Arbitrary payload serialized as JSON.
"""
status: str = "success"
message: Optional[str] = None
data: Optional[Dict[str, Any]] = None
class ParallelWorkerBase:
"""Base class for workloads executed across multiple worker processes.
The lifecycle is orchestrated by the main process:
* [`init()`][agentlightning.ParallelWorkerBase.init] prepares shared state.
* Each worker calls [`init_worker()`][agentlightning.ParallelWorkerBase.init_worker] during start-up.
* [`run()`][agentlightning.ParallelWorkerBase.run] performs the parallel workload.
* Workers call [`teardown_worker()`][agentlightning.ParallelWorkerBase.teardown_worker] before exiting.
* The main process finalizes through [`teardown()`][agentlightning.ParallelWorkerBase.teardown].
Subclasses must implement [`run()`][agentlightning.ParallelWorkerBase.run]
and can override other lifecycle hooks.
"""
def __init__(self) -> None:
"""Initialize the base class. This method can be overridden by subclasses."""
self.worker_id: Optional[int] = None
def init(self, *args: Any, **kwargs: Any) -> None:
"""Initialize before spawning the workers. This method can be overridden by subclasses."""
pass
def init_worker(self, worker_id: int, *args: Any, **kwargs: Any) -> None:
"""Initialize the worker. This method can be overridden by subclasses."""
self.worker_id = worker_id
def run(self, *args: Any, **kwargs: Any) -> Any:
"""Run the workload. This method can be overridden by subclasses."""
pass
def teardown_worker(self, worker_id: int, *args: Any, **kwargs: Any) -> None:
"""Teardown the worker. This method can be overridden by subclasses."""
pass
def teardown(self, *args: Any, **kwargs: Any) -> None:
"""Teardown after the workers have exited. This method can be overridden by subclasses."""
pass
class Dataset(Protocol, Generic[T_co]):
"""The general interface for a dataset.
It's currently implemented as a protocol, having a similar interface to `torch.utils.data.Dataset`.
You don't have to inherit from this class; you can use a simple list if you want to.
"""
def __getitem__(self, index: SupportsIndex, /) -> T_co: ...
def __len__(self) -> int: ...
class Hook(ParallelWorkerBase):
"""Base class for defining hooks in the agent runner's lifecycle."""
async def on_trace_start(
self, *, agent: LitAgent[Any], runner: Runner[Any], tracer: Tracer, rollout: Rollout
) -> None:
"""Hook called immediately after the tracer enters the trace context but before the rollout begins.
Args:
agent: The [`LitAgent`][agentlightning.LitAgent] instance associated with the runner.
runner: The [`Runner`][agentlightning.Runner] managing the rollout.
tracer: The [`Tracer`][agentlightning.Tracer] instance associated with the runner.
rollout: The [`Rollout`][agentlightning.Rollout] object that will be processed.
Subclasses can override this method to implement custom logic such as logging,
metric collection, or resource setup. By default, this is a no-op.
"""
async def on_trace_end(
self, *, agent: LitAgent[Any], runner: Runner[Any], tracer: Tracer, rollout: Rollout
) -> None:
"""Hook called immediately after the rollout completes but before the tracer exits the trace context.
Args:
agent: The [`LitAgent`][agentlightning.LitAgent] instance associated with the runner.
runner: The [`Runner`][agentlightning.Runner] managing the rollout.
tracer: The [`Tracer`][agentlightning.Tracer] instance associated with the runner.
rollout: The [`Rollout`][agentlightning.Rollout] object that has been processed.
Subclasses can override this method to implement custom logic such as logging,
metric collection, or resource cleanup. By default, this is a no-op.
"""
async def on_rollout_start(self, *, agent: LitAgent[Any], runner: Runner[Any], rollout: Rollout) -> None:
"""Hook called immediately before a rollout *attempt* begins.
Args:
agent: The [`LitAgent`][agentlightning.LitAgent] instance associated with the runner.
runner: The [`Runner`][agentlightning.Runner] managing the rollout.
rollout: The [`Rollout`][agentlightning.Rollout] object that will be processed.
Subclasses can override this method to implement custom logic such as
logging, metric collection, or resource setup. By default, this is a
no-op.
"""
async def on_rollout_end(
self,
*,
agent: LitAgent[Any],
runner: Runner[Any],
rollout: Rollout,
spans: Union[List[ReadableSpan], List[Span]],
) -> None:
"""Hook called after a rollout *attempt* completes.
Args:
agent: The [`LitAgent`][agentlightning.LitAgent] instance associated with the runner.
runner: The [`Runner`][agentlightning.Runner] managing the rollout.
rollout: The [`Rollout`][agentlightning.Rollout] object that has been processed.
spans: The spans that have been added to the store.
Subclasses can override this method for cleanup or additional
logging. By default, this is a no-op.
"""
class FilterField(TypedDict, total=False):
"""An operator dict for a single field."""
exact: Any
within: Sequence[Any]
contains: str
FilterOptions = Mapping[
Union[str, Literal["_aggregate", "_must"]],
Union[FilterField, Literal["and", "or"], Mapping[str, FilterField]],
]
"""A mapping of field name -> operator dict.
Each operator dict can contain:
- "exact": value for exact equality.
- "within": iterable of allowed values.
- "contains": substring to search for in string fields.
The filter can also have a special field called "_aggregate" that can be used to specify the logic
to combine the results of the filters:
- "and": all conditions must match. This is the default value if not specified.
- "or": at least one condition must match.
All conditions within a field and between different fields are
stored in a unified pool and combined using `_aggregate`.
The filter can also have a special group called "_must", which is a mapping of filters that must all match,
no matter whether the aggregate logic is "and" or "or".
Example:
```json
{
"_aggregate": "or",
"_must": {
"city": {"exact": "New York"},
"timezone": {"within": ["America/New_York", "America/Los_Angeles"]},
},
"status": {"exact": "active"},
"id": {"within": [1, 2, 3]},
"name": {"contains": "foo"},
}
```
"""
class SortOptions(TypedDict):
"""Options for sorting the collection."""
name: str
"""The name of the field to sort by."""
order: Literal["asc", "desc"]
"""The order to sort by."""
T_item = TypeVar("T_item")
class PaginatedResult(BaseModel, Sequence[T_item]):
"""Result of a paginated query.
Behaves like a sequence, but also carries pagination metadata (limit, offset, total).
"""
items: Sequence[T_item]
"""Items in the result."""
limit: int
"""Limit of the result."""
offset: int
"""Offset of the result."""
total: int
"""Total number of items in the collection."""
def __len__(self) -> int:
return len(self.items)
@overload
def __getitem__(self, index: int) -> T_item: ...
@overload
def __getitem__(self, index: slice) -> Sequence[T_item]: ...
def __getitem__(self, index: Union[int, slice]) -> Union[T_item, Sequence[T_item]]:
return self.items[index]
# Overriding __iter__ enables list(paginated_result) to work as expected,
# but changes Pydantic's default dict iteration behavior (which would otherwise
# iterate over field names).
def __iter__(self) -> Iterator[T_item]: # type: ignore
return iter(self.items)
def __repr__(self) -> str:
first_item_repr = repr(self.items[0]) if self.items else "empty"
items_repr = f"[{first_item_repr}, ...]" if len(self.items) > 1 else first_item_repr
slice_repr = f"{self.offset}:" if self.limit == -1 else f"{self.offset}:{self.offset + self.limit}"
return f"<PaginatedResult ({slice_repr} of {self.total}) {items_repr}>"
+204
View File
@@ -0,0 +1,204 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
"""Typed representations of tunable resources shared between Agent Lightning components."""
import inspect
import logging
from typing import (
Annotated,
Any,
Dict,
Literal,
Optional,
Union,
)
from pydantic import BaseModel, Field
from .core import AttemptedRollout
logger = logging.getLogger(__name__)
__all__ = [
"Resource",
"LLM",
"ProxyLLM",
"PromptTemplate",
"ResourceUnion",
"NamedResources",
"ResourcesUpdate",
]
class Resource(BaseModel):
"""Base class for tunable resources distributed to executors."""
resource_type: Any
"""Alias of the resource type."""
class LLM(Resource):
"""Resource that identifies an LLM endpoint and its configuration."""
resource_type: Literal["llm"] = "llm"
endpoint: str
"""The URL of the LLM API endpoint."""
model: str
"""The identifier for the model to be used (e.g., 'gpt-4o')."""
api_key: Optional[str] = None
"""Optional secret used to authenticate requests."""
sampling_parameters: Dict[str, Any] = Field(default_factory=dict)
"""A dictionary of hyperparameters for model inference, such as temperature, top_p, etc."""
def get_base_url(self, *args: Any, **kwargs: Any) -> str:
"""Return the base URL consumed by OpenAI-compatible clients.
Users are encouraged to use `get_base_url(rollout_id, attempt_id)` to get
the LLM endpoint instead of accessing `.endpoint` directly.
"""
return self.endpoint
class ProxyLLM(LLM):
"""LLM resource that rewrites endpoints through [`LLMProxy`][agentlightning.LLMProxy].
The proxy injects rollout- and attempt-specific routing information into the
endpoint so that downstream services can attribute requests correctly.
"""
resource_type: Literal["proxy_llm"] = "proxy_llm" # type: ignore
_initialized: bool = False
def model_post_init(self, __context: Any) -> None:
"""Mark initialization as complete after Pydantic finishes setup."""
super().model_post_init(__context)
object.__setattr__(self, "_initialized", True)
def __getattribute__(self, name: str) -> Any:
"""Emit a warning when `endpoint` is accessed directly after initialization."""
# Check if we're accessing endpoint after initialization and not from base_url
if name == "endpoint":
try:
initialized = object.__getattribute__(self, "_initialized")
except AttributeError:
initialized = False
if initialized:
# Check the call stack to see if we're being called from base_url
frame = inspect.currentframe()
if frame and frame.f_back:
caller_name = frame.f_back.f_code.co_name
if caller_name != "get_base_url":
logger.warning(
"Accessing 'endpoint' directly on ProxyLLM is discouraged. "
"Use 'get_base_url(rollout_id, attempt_id)' instead to get the properly formatted endpoint."
)
return super().__getattribute__(name)
def with_attempted_rollout(self, rollout: AttemptedRollout) -> LLM:
"""Bake rollout metadata into a concrete [`LLM`][agentlightning.LLM] instance."""
return LLM(
endpoint=self.get_base_url(rollout.rollout_id, rollout.attempt.attempt_id),
model=self.model,
sampling_parameters=self.sampling_parameters,
api_key=self.api_key,
)
def get_base_url(self, rollout_id: Optional[str], attempt_id: Optional[str]) -> str:
"""Return the routed endpoint for a specific rollout/attempt pair.
Args:
rollout_id: Identifier of the rollout making the request.
attempt_id: Identifier of the attempt within that rollout.
Returns:
Fully qualified endpoint including rollout metadata.
Raises:
ValueError: If exactly one of ``rollout_id`` or ``attempt_id`` is provided.
"""
if rollout_id is None and attempt_id is None:
return self.endpoint
if not (isinstance(rollout_id, str) and isinstance(attempt_id, str)):
raise ValueError("rollout_id and attempt_id must be strings or all be empty")
prefix = self.endpoint
if prefix.endswith("/"):
prefix = prefix[:-1]
if prefix.endswith("/v1"):
prefix = prefix[:-3]
has_v1 = True
else:
has_v1 = False
# Now the prefix should look like "http://localhost:11434"
# Append the rollout and attempt id to the prefix
prefix = prefix + f"/rollout/{rollout_id}/attempt/{attempt_id}"
if has_v1:
prefix = prefix + "/v1"
return prefix
class PromptTemplate(Resource):
"""Resource describing a reusable prompt template."""
resource_type: Literal["prompt_template"] = "prompt_template"
template: str
"""The template string. The format depends on the engine."""
engine: Literal["jinja", "f-string", "poml"]
"""The templating engine to use for rendering the prompt."""
def format(self, **kwargs: Any) -> str:
"""Format the prompt using keyword arguments.
!!! warning
Only the `f-string` engine is supported for now.
"""
if self.engine == "f-string":
return self.template.format(**kwargs)
else:
raise NotImplementedError(
"Formatting prompt templates for non-f-string engines with format() helper is not supported yet."
)
# Use discriminated union for proper deserialization
# TODO: migrate to use a registry
ResourceUnion = Annotated[Union[LLM, ProxyLLM, PromptTemplate], Field(discriminator="resource_type")]
NamedResources = Dict[str, ResourceUnion]
"""Mapping from resource names to their configured instances.
Examples:
```python
resources: NamedResources = {
"main_llm": LLM(
endpoint="http://localhost:8080",
model="llama3",
sampling_parameters={"temperature": 0.7, "max_tokens": 100},
),
"system_prompt": PromptTemplate(
template="You are a helpful assistant.",
engine="f-string",
),
}
```
"""
class ResourcesUpdate(BaseModel):
"""Update payload broadcast to clients when resources change."""
resources_id: str
"""Identifier used to version the resources."""
create_time: float
"""Timestamp of the creation time of the resources."""
update_time: float
"""Timestamp of the last update time of the resources."""
version: int
"""Version of the resources."""
resources: NamedResources
"""Mapping of resource names to their definitions."""
+512
View File
@@ -0,0 +1,512 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import time
"""Data models that mirror OpenTelemetry spans for Agent Lightning."""
import json
from enum import Enum
from typing import Any, Dict, List, Literal, Optional, Protocol, Sequence, Union
from opentelemetry import trace as trace_api
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import Event as OtelEvent
from opentelemetry.sdk.trace import ReadableSpan
from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
from opentelemetry.trace.status import Status as OtelStatus
from pydantic import BaseModel, ConfigDict
from agentlightning.semconv import AGL_VIRTUAL
__all__ = [
"AttributeValue",
"Attributes",
"TraceState",
"SpanContext",
"TraceStatus",
"Event",
"Link",
"OtelResource",
"Span",
"SpanNames",
"SpanAttributeNames",
"SpanLike",
"StatusCode",
"SpanCoreFields",
"SpanRecordingContext",
]
def convert_timestamp(timestamp: Optional[int]) -> Optional[float]:
"""Normalize OpenTelemetry timestamps to seconds.
Args:
timestamp: Timestamp expressed either in seconds or nanoseconds.
Returns:
Timestamp in seconds when `timestamp` is provided; otherwise `None`.
"""
if not timestamp:
return None
return timestamp / 1_000_000_000 if timestamp > 1e12 else timestamp
def extract_extra_fields(src: Any, excluded_fields: List[str]) -> Dict[str, Any]:
"""Capture custom attributes from an OpenTelemetry object.
Args:
src: Object that exposes a `__dict__` of potential attributes.
excluded_fields: Attribute names that should be removed from the output.
Returns:
Dictionary containing JSON-serializable representations of the remaining fields.
"""
excluded_fields_set = set(excluded_fields) | set(["_" + k for k in excluded_fields])
# Exclude the function fields
excluded_fields_set |= set(src.__class__.__dict__.keys())
stripped_dict = {k.lstrip("_"): v for k, v in src.__dict__.items()}
candidates = {k: v for k, v in stripped_dict.items() if k not in excluded_fields_set and not k.startswith("_")}
# This should strip or flatten the unserializable fields
candidates_serialized = json.dumps(candidates, default=str)
return json.loads(candidates_serialized)
AttributeValue = Union[
str,
bool,
int,
float,
Sequence[str],
Sequence[bool],
Sequence[int],
Sequence[float],
]
"""Possible values for OpenTelemetry attributes."""
Attributes = Dict[str, AttributeValue]
"""Mapping from attribute names to their values. Same as OpenTelemetry `Attributes` type."""
TraceState = Dict[str, str]
"""Mapping from trace state key to its value. Same as OpenTelemetry `TraceState` type."""
StatusCode = Literal["UNSET", "OK", "ERROR"]
"""The status code of the span."""
class SpanContext(BaseModel):
"""Pydantic representation of `opentelemetry.trace.SpanContext` values."""
trace_id: str
"""The trace ID of the span."""
span_id: str
"""The span ID of the span."""
is_remote: bool
"""Whether the span is remote."""
trace_state: TraceState
"""Mapping from trace state key to its value."""
model_config = ConfigDict(extra="allow")
@classmethod
def from_opentelemetry(cls, src: trace_api.SpanContext) -> "SpanContext":
"""Construct a [`SpanContext`][agentlightning.SpanContext] from OpenTelemetry data."""
return cls(
trace_id=trace_api.format_trace_id(src.trace_id),
span_id=trace_api.format_span_id(src.span_id),
is_remote=src.is_remote,
trace_state={k: v for k, v in src.trace_state.items()} if src.trace_state else {},
**extract_extra_fields(src, ["trace_id", "span_id", "is_remote", "trace_state"]),
)
class TraceStatus(BaseModel):
"""Serializable variant of `opentelemetry.trace.Status`."""
status_code: StatusCode
"""The status code of the span. Same as OpenTelemetry `Status.status_code` type."""
description: Optional[str] = None
"""The description of the span. Same as OpenTelemetry `Status.description` type."""
model_config = ConfigDict(extra="allow")
@classmethod
def from_opentelemetry(cls, src: OtelStatus) -> "TraceStatus":
"""Create a [`TraceStatus`][agentlightning.TraceStatus] from OpenTelemetry metadata."""
return cls(
status_code=src.status_code.name,
description=src.description,
**extract_extra_fields(src, ["status_code", "description"]),
)
class Event(BaseModel):
"""Serializable representation of OpenTelemetry `Event` values."""
name: str
"""The name of the event."""
attributes: Attributes
"""Mapping from attribute names to their values. Same as OpenTelemetry `Attributes` type."""
timestamp: Optional[float] = None
"""The timestamp of the event. Same as OpenTelemetry `Event.timestamp` type."""
model_config = ConfigDict(extra="allow")
@classmethod
def from_opentelemetry(cls, src: OtelEvent) -> "Event":
"""Create an [`Event`][agentlightning.Event] from an OpenTelemetry event."""
return cls(
name=src.name,
attributes=dict(src.attributes) if src.attributes else {},
timestamp=convert_timestamp(src.timestamp),
**extract_extra_fields(src, ["name", "attributes", "timestamp"]),
)
class Link(BaseModel):
"""Serializable representation of OpenTelemetry `Link` values."""
context: SpanContext
"""The context of the link."""
attributes: Optional[Attributes] = None
"""Optional attributes."""
model_config = ConfigDict(extra="allow")
@classmethod
def from_opentelemetry(cls, src: trace_api.Link) -> "Link":
"""Create a [`Link`][agentlightning.Link] from an OpenTelemetry link."""
return cls(
context=SpanContext.from_opentelemetry(src.context),
attributes=dict(src.attributes) if src.attributes else None,
**extract_extra_fields(src, ["context", "attributes"]),
)
class OtelResource(BaseModel):
"""Serializable representation of OpenTelemetry `Resource` values.
Named as `OtelResource` to avoid confusion with the [`Resource`][agentlightning.Resource] class.
Users will very rarely need to construct this class directly. Most of the times,
they deal with the [`Resource`][agentlightning.Resource] class instead, which describes
a very different concept.
"""
attributes: Attributes
"""Mapping from attribute names to their values. Same as OpenTelemetry `Attributes` type."""
schema_url: str
"""The schema URL of the resource."""
@classmethod
def from_opentelemetry(cls, src: Resource) -> "OtelResource":
"""Create a [`Resource`][agentlightning.Resource] from an OpenTelemetry resource."""
return cls(
attributes=dict(src.attributes) if src.attributes else {},
schema_url=src.schema_url if src.schema_url else "",
**extract_extra_fields(src, ["attributes", "schema_url"]),
)
class SpanCoreFields(BaseModel):
"""Core fields of a span. Used by span creators who don't care about the full span model.
If the spans are managed by some OTel tracer provider, it's not advised to create spans via this path.
"""
name: str
"""The name of the span."""
status: TraceStatus
"""The status of the span."""
attributes: Attributes
"""The attributes of the span."""
start_time: Optional[float]
"""The start time of the span."""
end_time: Optional[float]
"""The end time of the span."""
class SpanRecordingContext(Protocol):
"""Context for recording operations on a span. It doesn't have to finalize the span; the caller will do it."""
def record_exception(self, exception: BaseException) -> None:
"""Record an exception on the span."""
raise NotImplementedError()
def record_attributes(self, attributes: Attributes) -> None:
"""Record attributes on the span."""
raise NotImplementedError()
def record_status(self, status_code: StatusCode, description: Optional[str] = None) -> None:
"""Record the status of the span."""
raise NotImplementedError()
def get_recorded_span(self) -> SpanCoreFields:
"""Get the recording of the span."""
raise NotImplementedError()
class Span(BaseModel):
"""Agent Lightning's canonical span model used for persistence and analytics.
The model captures the most relevant fields from
`opentelemetry.sdk.trace.ReadableSpan` instances while preserving unmodeled
attributes in Pydantic `BaseModel`'s extra storage. This keeps the serialized format
stable even as upstream OpenTelemetry types evolve.
"""
model_config = ConfigDict(extra="allow")
rollout_id: str
"""The rollout which this span belongs to."""
attempt_id: str
"""The attempt which this span belongs to."""
sequence_id: int
"""The ID to make spans ordered within a single attempt."""
# Current ID (in hex, formatted via trace_api.format_*)
trace_id: str # one rollout can have traces coming from multiple places
"""The trace ID of the span. One rollout/attempt can have multiple traces.
This ID comes from the OpenTelemetry trace ID generator.
"""
span_id: str
"""The span ID of the span. This ID comes from the OpenTelemetry span ID generator."""
parent_id: Optional[str]
"""The parent span ID of the span."""
# Core ReadableSpan fields
name: str
"""The name of the span. See [OpenTelemetry docs](https://opentelemetry.io/docs/concepts/signals/traces/)."""
status: TraceStatus
"""The status of the span. See [OpenTelemetry docs](https://opentelemetry.io/docs/concepts/signals/traces/)."""
attributes: Attributes
"""The attributes of the span. See [OpenTelemetry docs](https://opentelemetry.io/docs/concepts/signals/traces/)."""
events: List[Event]
"""The events of the span. See [OpenTelemetry docs](https://opentelemetry.io/docs/concepts/signals/traces/)."""
links: List[Link]
"""The links of the span. See [OpenTelemetry docs](https://opentelemetry.io/docs/concepts/signals/traces/)."""
# Timestamps
start_time: Optional[float]
"""The start time of the span. See [OpenTelemetry docs](https://opentelemetry.io/docs/concepts/signals/traces/)."""
end_time: Optional[float]
"""The end time of the span. See [OpenTelemetry docs](https://opentelemetry.io/docs/concepts/signals/traces/)."""
# Other parsable fields
context: Optional[SpanContext]
"""The context of the span. See [OpenTelemetry docs](https://opentelemetry.io/docs/concepts/signals/traces/)."""
parent: Optional[SpanContext]
"""The parent context of the span. See [OpenTelemetry docs](https://opentelemetry.io/docs/concepts/signals/traces/)."""
resource: OtelResource
"""The resource of the span. See [OpenTelemetry docs](https://opentelemetry.io/docs/concepts/signals/traces/)."""
# Preserve other fields in the readable span as extra fields
# Make sure that are json serializable (so no bytes, complex objects, ...)
@classmethod
def from_opentelemetry(
cls,
src: ReadableSpan,
rollout_id: str,
attempt_id: str,
sequence_id: int,
) -> "Span":
"""Convert an OpenTelemetry span into the Agent Lightning data model.
Args:
src: Span captured by OpenTelemetry.
rollout_id: Identifier for the rollout that produced the span.
attempt_id: Identifier of the attempt within the rollout.
sequence_id: Monotonically increasing identifier assigned to the span.
Returns:
Parsed [`Span`][agentlightning.Span] instance suitable for persistence.
"""
context = src.get_span_context()
if context is None:
trace_id = span_id = 0
else:
trace_id = context.trace_id
span_id = context.span_id
return cls(
rollout_id=rollout_id,
attempt_id=attempt_id,
sequence_id=sequence_id,
trace_id=trace_api.format_trace_id(trace_id),
span_id=trace_api.format_span_id(span_id),
parent_id=(trace_api.format_span_id(src.parent.span_id) if src.parent else None),
name=src.name,
status=TraceStatus.from_opentelemetry(src.status),
attributes=dict(src.attributes) if src.attributes else {},
events=[Event.from_opentelemetry(event) for event in src.events] if src.events else [],
links=[Link.from_opentelemetry(link) for link in src.links] if src.links else [],
start_time=convert_timestamp(src.start_time),
end_time=convert_timestamp(src.end_time),
context=SpanContext.from_opentelemetry(context) if context else None,
parent=(SpanContext.from_opentelemetry(src.parent) if src.parent else None),
resource=OtelResource.from_opentelemetry(src.resource),
**extract_extra_fields(
src,
[
"name",
"context",
"parent",
"resource",
"attributes",
"events",
"links",
"start_time",
"end_time",
"status",
"span_processor",
"rollout_id",
"attempt_id",
"trace_id",
"span_id",
"parent_id",
],
),
)
@classmethod
def from_attributes(
cls,
*,
attributes: Attributes,
rollout_id: Optional[str] = None,
attempt_id: Optional[str] = None,
sequence_id: Optional[int] = None,
name: Optional[str] = None,
trace_id: Optional[str] = None,
span_id: Optional[str] = None,
parent_id: Optional[str] = None,
start_time: Optional[float] = None,
end_time: Optional[float] = None,
resource: Optional[OtelResource] = None,
status: Optional[TraceStatus] = None,
) -> "Span":
"""Build a synthetic span from raw attributes.
Different from the [`from_opentelemetry`][agentlightning.Span.from_opentelemetry] method,
all parameters other than `attributes` are optional and will be generated if not provided.
Args:
attributes: Span attributes to persist.
rollout_id: Optional rollout identifier associated with the span.
attempt_id: Optional attempt identifier associated with the span.
sequence_id: Optional sequence number to preserve ordering.
name: Optional human-readable span name.
trace_id: Custom trace identifier. When omitted, a random identifier is generated.
span_id: Custom span identifier. When omitted, a random identifier is generated.
parent_id: Optional parent span identifier.
start_time: Span start timestamp in seconds.
end_time: Span end timestamp in seconds.
resource: Explicit resource information to attach to the span.
status: Optional status of the span.
Returns:
[`Span`][agentlightning.Span] populated with the provided attributes.
"""
id_generator = RandomIdGenerator()
trace_id = trace_id or trace_api.format_trace_id(id_generator.generate_trace_id())
span_id = span_id or trace_api.format_span_id(id_generator.generate_span_id())
return cls(
rollout_id=rollout_id or "",
attempt_id=attempt_id or "",
sequence_id=sequence_id or 0,
trace_id=trace_id,
span_id=span_id,
parent_id=parent_id,
start_time=start_time,
end_time=end_time,
context=SpanContext(
trace_id=trace_id,
span_id=span_id,
is_remote=False,
trace_state={},
),
name=name or AGL_VIRTUAL,
resource=resource or OtelResource(attributes={}, schema_url=""),
attributes=attributes,
status=status or TraceStatus(status_code="OK"),
events=[],
links=[],
parent=(
SpanContext(
trace_id=trace_id,
span_id=parent_id,
is_remote=False,
trace_state={},
)
if parent_id
else None
),
)
@classmethod
def from_core_fields(
cls,
core: SpanCoreFields,
*,
rollout_id: Optional[str] = None,
attempt_id: Optional[str] = None,
sequence_id: Optional[int] = None,
) -> Span:
"""Build a span from a core span.
Args:
core: Core span to build from.
rollout_id: Optional rollout identifier associated with the span.
attempt_id: Optional attempt identifier associated with the span.
sequence_id: Optional sequence number to preserve ordering.
Returns:
[`Span`][agentlightning.Span] populated with the provided attributes.
"""
return cls.from_attributes(
attributes=core.attributes,
rollout_id=rollout_id,
attempt_id=attempt_id,
sequence_id=sequence_id,
name=core.name,
start_time=core.start_time or time.time(),
end_time=core.end_time,
status=core.status,
)
class SpanNames(str, Enum):
"""Enumerated span names recognised by Agent-lightning. Deprecated in favor of [semconv][agentlightning.semconv]."""
REWARD = "agentlightning.reward"
"""The name of the reward span."""
MESSAGE = "agentlightning.message"
"""The name of the message span."""
OBJECT = "agentlightning.object"
"""The name of the object span."""
EXCEPTION = "agentlightning.exception"
"""The name of the exception span."""
VIRTUAL = "agentlightning.virtual"
"""The name of the virtual span. It represents derived spans without concrete operations."""
ROLLOUT_ID = "agentlightning.rollout_id"
"""The name of the rollout ID."""
ATTEMPT_ID = "agentlightning.attempt_id"
"""The name of the attempt ID."""
SPAN_SEQUENCE_ID = "agentlightning.span_sequence_id"
"""The name of the span sequence ID."""
class SpanAttributeNames(str, Enum):
"""Canonical attribute names written by Agent Lightning emitters. Deprecated in favor of [semconv][agentlightning.semconv]."""
MESSAGE = "message"
"""The name of the message attribute."""
OBJECT = "object"
"""The name of the object attribute."""
SpanLike = Union[ReadableSpan, Span]
"""Union type of OpenTelemetry `ReadableSpan` and Agent-lightning [`Span`][agentlightning.Span]."""