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
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:
@@ -0,0 +1,249 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Sequence
|
||||
|
||||
import pytest
|
||||
|
||||
from agentlightning.adapter import TraceAdapter
|
||||
from agentlightning.algorithm import Baseline
|
||||
from agentlightning.store.memory import InMemoryLightningStore
|
||||
from agentlightning.types import (
|
||||
LLM,
|
||||
NamedResources,
|
||||
OtelResource,
|
||||
Span,
|
||||
SpanContext,
|
||||
TraceStatus,
|
||||
)
|
||||
|
||||
LOGGER_NAME = "agentlightning.algorithm.fast"
|
||||
|
||||
|
||||
class _AdapterStub(TraceAdapter[Dict[str, Any]]):
|
||||
def adapt(self, source: Sequence[Span], /) -> Dict[str, Any]:
|
||||
return {
|
||||
"count": len(source),
|
||||
"attempt_ids": sorted({span.attempt_id for span in source}),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class _RolloutArtifacts:
|
||||
rollout_id: str
|
||||
attempt_id: str
|
||||
attempt_sequence: int
|
||||
span: Span
|
||||
|
||||
|
||||
def _make_resources() -> NamedResources:
|
||||
return {
|
||||
"main_llm": LLM(endpoint="http://localhost", model="test-model"),
|
||||
}
|
||||
|
||||
|
||||
def _build_span(rollout_id: str, attempt_id: str, *, sequence_id: int, index: int) -> Span:
|
||||
trace_hex = f"{index:032x}"
|
||||
span_hex = f"{index:016x}"
|
||||
# Minimal span that passes validation and keeps log output predictable.
|
||||
return Span(
|
||||
rollout_id=rollout_id,
|
||||
attempt_id=attempt_id,
|
||||
sequence_id=sequence_id,
|
||||
trace_id=trace_hex,
|
||||
span_id=span_hex,
|
||||
parent_id=None,
|
||||
name="test-span",
|
||||
status=TraceStatus(status_code="OK"),
|
||||
attributes={"stage": "collect"},
|
||||
events=[],
|
||||
links=[],
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
context=SpanContext(trace_id=trace_hex, span_id=span_hex, is_remote=False, trace_state={}),
|
||||
parent=None,
|
||||
resource=OtelResource(attributes={}, schema_url=""),
|
||||
)
|
||||
|
||||
|
||||
async def _mock_runner(
|
||||
*,
|
||||
store: InMemoryLightningStore,
|
||||
expected: int,
|
||||
artifacts: List[_RolloutArtifacts],
|
||||
) -> None:
|
||||
"""Simulate a runner consuming rollouts, adding spans, and marking them complete."""
|
||||
processed = 0
|
||||
while processed < expected:
|
||||
attempted = await store.dequeue_rollout()
|
||||
if attempted is None:
|
||||
await asyncio.sleep(0.001)
|
||||
continue
|
||||
|
||||
attempt = attempted.attempt
|
||||
rollout_id = attempted.rollout_id
|
||||
await store.update_attempt(
|
||||
rollout_id,
|
||||
attempt.attempt_id,
|
||||
status="running",
|
||||
worker_id="runner-1",
|
||||
)
|
||||
|
||||
span = _build_span(rollout_id, attempt.attempt_id, sequence_id=1, index=processed + 1)
|
||||
await store.add_span(span)
|
||||
await store.update_attempt(rollout_id, attempt.attempt_id, status="succeeded")
|
||||
await store.update_rollout(rollout_id, status="succeeded")
|
||||
|
||||
artifacts.append(
|
||||
_RolloutArtifacts(
|
||||
rollout_id=rollout_id,
|
||||
attempt_id=attempt.attempt_id,
|
||||
attempt_sequence=attempt.sequence_id,
|
||||
span=span,
|
||||
)
|
||||
)
|
||||
processed += 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mock_algorithm_collects_rollout_logs(caplog: pytest.LogCaptureFixture) -> None:
|
||||
store = InMemoryLightningStore()
|
||||
await store.update_resources("default", _make_resources())
|
||||
algorithm = Baseline(polling_interval=0.01, span_verbosity="key_values")
|
||||
algorithm.set_store(store)
|
||||
adapter = _AdapterStub()
|
||||
algorithm.set_adapter(adapter)
|
||||
|
||||
caplog.set_level(logging.INFO, logger=LOGGER_NAME)
|
||||
|
||||
train_dataset = ["train-sample", "validation-sample"]
|
||||
expected_rollouts = len(train_dataset)
|
||||
artifacts: List[_RolloutArtifacts] = []
|
||||
|
||||
runner_task = asyncio.create_task(_mock_runner(store=store, expected=expected_rollouts, artifacts=artifacts))
|
||||
try:
|
||||
await algorithm.run(train_dataset=train_dataset)
|
||||
await asyncio.wait_for(runner_task, timeout=2)
|
||||
finally:
|
||||
if not runner_task.done():
|
||||
runner_task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await runner_task
|
||||
|
||||
log_messages = [record.getMessage() for record in caplog.records if record.name == LOGGER_NAME]
|
||||
|
||||
# Ensure final status, attempt details, span details, and adapter output are logged per rollout.
|
||||
for entry in artifacts:
|
||||
attempt_summary = (
|
||||
f"[Rollout {entry.rollout_id} | Attempt {entry.attempt_sequence}] "
|
||||
f"ID: {entry.attempt_id}. Status: succeeded. Worker: runner-1"
|
||||
)
|
||||
assert attempt_summary in log_messages
|
||||
|
||||
span_prefix = (
|
||||
f"[Rollout {entry.rollout_id} | Attempt {entry.attempt_id} | Span {entry.span.span_id}] "
|
||||
f"#{entry.span.sequence_id} ({entry.span.name}) "
|
||||
)
|
||||
assert any(msg.startswith(span_prefix + "From") for msg in log_messages)
|
||||
assert any(f"Attributes: {entry.span.attributes}" in msg for msg in log_messages)
|
||||
assert any(
|
||||
msg.startswith(f"[Rollout {entry.rollout_id}] Finished with status succeeded") for msg in log_messages
|
||||
)
|
||||
assert any(
|
||||
msg.startswith(f"[Rollout {entry.rollout_id}] Adapted data: ")
|
||||
and "'count': 1" in msg
|
||||
and entry.attempt_id in msg
|
||||
for msg in log_messages
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_baseline_does_not_skip_samples_when_queue_full() -> None:
|
||||
"""Test that Baseline waits and retries when queue is full instead of skipping samples.
|
||||
|
||||
This is a regression test for a bug where samples would be skipped when the queue
|
||||
exceeded max_queue_length. The fix wraps the queue check in a while loop to ensure
|
||||
all samples are eventually processed.
|
||||
"""
|
||||
store = InMemoryLightningStore()
|
||||
await store.update_resources("default", _make_resources())
|
||||
|
||||
# Use a small max_queue_length and fast polling to test queue full behavior
|
||||
algorithm = Baseline(polling_interval=0.01, max_queue_length=1)
|
||||
algorithm.set_store(store)
|
||||
|
||||
# Create a dataset with 5 samples
|
||||
train_dataset = [f"sample-{i}" for i in range(5)]
|
||||
expected_rollouts = len(train_dataset)
|
||||
|
||||
# Track which samples were enqueued
|
||||
enqueued_samples: List[Any] = []
|
||||
artifacts: List[_RolloutArtifacts] = []
|
||||
|
||||
async def _slow_runner() -> None:
|
||||
"""A slow runner that creates backpressure by processing rollouts with delays."""
|
||||
processed = 0
|
||||
while processed < expected_rollouts:
|
||||
attempted = await store.dequeue_rollout()
|
||||
if attempted is None:
|
||||
await asyncio.sleep(0.01)
|
||||
continue
|
||||
|
||||
attempt = attempted.attempt
|
||||
rollout_id = attempted.rollout_id
|
||||
rollout = await store.get_rollout_by_id(rollout_id)
|
||||
|
||||
# Track the sample that was enqueued
|
||||
if rollout:
|
||||
enqueued_samples.append(rollout.input)
|
||||
|
||||
await store.update_attempt(
|
||||
rollout_id,
|
||||
attempt.attempt_id,
|
||||
status="running",
|
||||
worker_id="slow-runner",
|
||||
)
|
||||
|
||||
# Add a delay to create backpressure and cause queue to fill up
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
span = _build_span(rollout_id, attempt.attempt_id, sequence_id=1, index=processed + 1)
|
||||
await store.add_span(span)
|
||||
await store.update_attempt(rollout_id, attempt.attempt_id, status="succeeded")
|
||||
await store.update_rollout(rollout_id, status="succeeded")
|
||||
|
||||
artifacts.append(
|
||||
_RolloutArtifacts(
|
||||
rollout_id=rollout_id,
|
||||
attempt_id=attempt.attempt_id,
|
||||
attempt_sequence=attempt.sequence_id,
|
||||
span=span,
|
||||
)
|
||||
)
|
||||
processed += 1
|
||||
|
||||
runner_task = asyncio.create_task(_slow_runner())
|
||||
try:
|
||||
await algorithm.run(train_dataset=train_dataset)
|
||||
await asyncio.wait_for(runner_task, timeout=5)
|
||||
finally:
|
||||
if not runner_task.done():
|
||||
runner_task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await runner_task
|
||||
|
||||
# Verify that ALL samples were enqueued and processed (no samples skipped)
|
||||
assert (
|
||||
len(enqueued_samples) == expected_rollouts
|
||||
), f"Expected {expected_rollouts} samples to be enqueued, but got {len(enqueued_samples)}"
|
||||
assert (
|
||||
len(artifacts) == expected_rollouts
|
||||
), f"Expected {expected_rollouts} rollouts to be processed, but got {len(artifacts)}"
|
||||
|
||||
# Verify that the enqueued samples match the dataset (in order)
|
||||
assert (
|
||||
enqueued_samples == train_dataset
|
||||
), f"Enqueued samples {enqueued_samples} do not match dataset {train_dataset}"
|
||||
Reference in New Issue
Block a user