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

368 lines
16 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
import multiprocessing
import signal
import time
import warnings
from typing import Any, List, Optional, TypeVar, Union
from agentlightning.adapter import TraceAdapter, TracerTraceToTriplet
from agentlightning.algorithm import Algorithm
from agentlightning.client import AgentLightningClient
from agentlightning.litagent import LitAgent
from agentlightning.runner import LegacyAgentRunner
from agentlightning.tracer.base import Tracer
from agentlightning.types import Dataset, ParallelWorkerBase
logger = logging.getLogger(__name__)
T_co = TypeVar("T_co", covariant=True)
class TrainerLegacy(ParallelWorkerBase):
"""Trainer for legacy mode for v0.1 compatibility."""
def __init__(self, *args: Any, **kwargs: Any):
"""Initialize the TrainerLegacy.
This method is mainly to make type checker happy.
It won't be used in practice.
"""
self._dev = kwargs.pop("dev", False)
self.algorithm: Optional[Algorithm] = kwargs.pop("algorithm", None)
self.tracer: Tracer = kwargs.pop("tracer", None)
self.n_workers: int = kwargs.pop("n_workers", None)
self.max_tasks: Optional[int] = kwargs.pop("max_tasks", None)
self.daemon: bool = kwargs.pop("daemon", True)
self.triplet_exporter: TraceAdapter[Any] = kwargs.pop("triplet_exporter", None)
def _extract_client_from_data(
self, data: Union[str, AgentLightningClient, Dataset[Any]]
) -> Optional[AgentLightningClient]:
"""Extract client from data if it's a string URL or AgentLightningClient."""
if isinstance(data, str):
if not data.startswith("http://") and not data.startswith("https://"):
raise ValueError("String data must be a valid URL starting with http:// or https://")
return AgentLightningClient(endpoint=data)
elif isinstance(data, AgentLightningClient):
return data
return None
def _extract_dataset_from_data(
self, data: Union[str, AgentLightningClient, Dataset[Any]]
) -> Optional[Dataset[Any]]:
"""Extract dataset from data if it's a Dataset."""
if isinstance(data, str) or isinstance(data, AgentLightningClient):
return None
return data
def _determine_backend(
self,
train_data: Union[str, AgentLightningClient, Dataset[Any]],
dev_data: Union[str, AgentLightningClient, Dataset[Any], None] = None,
) -> Union[str, AgentLightningClient]:
"""Determine which backend to use for initialization."""
if self._dev:
if dev_data is None:
raise ValueError("dev_data must be provided when dev=True.")
client = self._extract_client_from_data(dev_data)
if client is None:
raise ValueError("dev_data must be a string URL or AgentLightningClient when dev=True.")
return client
else:
client = self._extract_client_from_data(train_data)
if client is None and self.algorithm is None:
raise ValueError(
"train_data must be a string URL or AgentLightningClient when no algorithm is provided."
)
elif client is None and self.algorithm is not None:
# Algorithm will be responsible for creating the client
client = self.algorithm.get_client()
logger.info(f"Algorithm created client: {client}")
return client
if client is None:
raise ValueError(
"train_data must be a string URL or AgentLightningClient when no algorithm is provided."
)
return client
def init(self, backend: Union[str, AgentLightningClient]) -> None:
logger.info(f"Initializing Trainer...")
self._init_client(backend)
self.tracer.init()
logger.info(f"Trainer main initialization complete.")
def teardown(self) -> None:
logger.info(f"Cleaning up Trainer...")
self.tracer.teardown()
self._client = None
logger.info(f"Trainer main cleanup complete.")
def client(self) -> AgentLightningClient:
"""Returns the AgentLightningClient instance."""
if self._client is None:
raise RuntimeError("AgentLightningClient has not been initialized. Call `init` first.")
return self._client
def _init_client(self, backend: Union[str, AgentLightningClient]) -> AgentLightningClient:
if self._client is None:
if isinstance(backend, AgentLightningClient):
logger.info("Using provided AgentLightningClient instance.")
self._client = backend
else:
logger.info(f"Initializing AgentLightningClient with endpoint: {backend}")
if not isinstance(backend, str): # type: ignore
raise ValueError("backend must be a string URL or an AgentLightningClient instance.")
if not backend.startswith("http://") and not backend.startswith("https://"):
raise ValueError("backend must be a valid URL starting with http:// or https://")
# Initialize the client with the provided backend URL
self._client = AgentLightningClient(endpoint=backend)
else:
logger.warning("AgentLightningClient already initialized. Returning existing instance.")
return self._client
def _worker_main_loop(self, agent: LitAgent[Any], worker_id: int, is_async: bool):
"""The main function for each worker process.
This function initializes the client and the loop, then starts the
execution. It also configures process-specific settings like the
process title and signal handling.
Args:
agent: The `LitAgent` instance to run.
worker_id: The unique ID for this worker.
is_async: A boolean indicating if the async loop should be run.
"""
if self.n_workers > 1:
import setproctitle
# Ignore Ctrl+C in worker processes; the main process handles it
signal.signal(signal.SIGINT, signal.SIG_IGN)
setproctitle.setproctitle(multiprocessing.current_process().name)
# Now we are in child processes, so we can safely set up the environment.
agent.set_trainer(self) # type: ignore
if not isinstance(self.triplet_exporter, TracerTraceToTriplet): # type: ignore
raise ValueError("triplet_exporter must be a TracerTraceToTriplet for the legacy trainer.")
# TODO: this should be set elsewhere
if agent.trained_agents:
self.triplet_exporter.agent_match = agent.trained_agents
self._initialize_worker_env(worker_id)
mode = "Async" if is_async else "Sync"
logger.info(f"[Worker {worker_id}] {mode} worker process started.")
num_processed = 0
try:
client = self.client()
loop = LegacyAgentRunner(
agent=agent,
client=client,
tracer=self.tracer,
triplet_exporter=self.triplet_exporter,
max_tasks=self.max_tasks,
worker_id=worker_id,
)
loop.init_worker(worker_id) # type: ignore
if is_async:
num_processed = asyncio.run(loop.iter_async())
else:
num_processed = loop.iter()
except Exception:
logger.exception(f"[Worker {worker_id}] Unhandled exception in worker loop.")
finally:
self._teardown_worker_env(worker_id)
return num_processed
def _initialize_worker_env(self, worker_id: int):
logger.info(f"[Worker {worker_id}] Setting up trainer environment...") # worker_id included in process name
self.tracer.init_worker(worker_id)
def _teardown_worker_env(self, worker_id: int):
logger.info(f"[Worker {worker_id}] Cleaning up trainer environment...")
self.tracer.teardown_worker(worker_id)
logger.info(f"[Worker {worker_id}] Environment cleanup complete.")
@staticmethod
def kill_orphaned_processes() -> None:
"""
Kill any orphaned processes that may have been left behind by previous runs.
This is useful for cleaning up after crashes or unexpected exits.
"""
import psutil
for proc in psutil.process_iter(): # type: ignore
# check whether the process name matches
if proc.name().startswith("AgentLightning-"):
proc.kill()
def _terminate_processes(self, processes: List[multiprocessing.Process]) -> None:
if self.n_workers > 1 and len(processes) > 0:
for i, p in enumerate(processes):
if p.is_alive():
logger.info(f"Terminating worker {i} (name: {p.name}, PID: {p.pid})...")
p.terminate()
else:
logger.info(f"Worker {i} (name: {p.name}, PID: {p.pid}) is not alive or has already terminated.")
for i, p in enumerate(processes):
if p.is_alive():
p.join(timeout=10) # Give some time to terminate
if p.is_alive(): # If still alive, kill
logger.warning(
f"Worker {i} (name: {p.name}, PID: {p.pid}) did not terminate gracefully, killing..."
)
p.kill()
p.join(timeout=10) # Ensure it's reaped
def fit_v0(
self,
agent: LitAgent[T_co],
train_data: Union[str, AgentLightningClient, Dataset[T_co]],
*,
val_data: Union[str, AgentLightningClient, Dataset[T_co], None] = None,
dev_data: Union[str, AgentLightningClient, Dataset[T_co], None] = None,
dev_backend: Union[str, AgentLightningClient, None] = None,
):
"""Train the agent using the provided data.
Each data argument can be a string URL connecting to a agent-lightning server,
or an AgentLightningClient instance connecting to a server (or mock server), or a dataset.
If no algorithm is provided when instantiating the trainer, the data must be
provided to connecting a server. Otherwise, dataset is also allowed and will be
passed to the algorithm.
If the algorithm is instantiated and there is no URL/client provided,
the algorithm will be responsible for creating a client that will connect to itself.
It can also create a mock client if the algorithm does not require a server.
"""
if dev_backend is not None:
warnings.warn("dev_backend is deprecated. Use dev_data instead.")
if dev_data is not None:
raise ValueError("dev_data and dev_backend cannot be provided at the same time.")
dev_data = dev_backend
# Extract datasets for algorithm if available
train_dataset = self._extract_dataset_from_data(train_data)
val_dataset = self._extract_dataset_from_data(val_data) if val_data else None
# Initialize the algorithm with trainer if provided
if self.algorithm is not None:
self.algorithm.set_trainer(self) # type: ignore
# DO NOT RUN TRAINING HERE. Need to spawn the worker first.
# Determine the backend to use for client-server mode
backend = self._determine_backend(train_data, dev_data)
if self._dev:
logger.warning(f"Running in dev mode. Using dev backend: {backend}")
else:
logger.debug(f"Running in non-dev mode. Using backend: {backend}")
self.init(backend)
processes: List[multiprocessing.Process] = []
# Determine if the agent is asynchronous
mode = "asynchronous" if agent.is_async() else "synchronous"
try:
if self.n_workers == 1:
logger.info(f"Running with n_workers=1 ({mode} in main process).")
# Warn if algorithm is set with single worker mode
if self.algorithm is not None:
logger.warning(
"Algorithm is set but using single worker mode. Algorithm will never get the chance to run."
)
# Ideally the single worker should be run in a separate thread or process.
num_tasks = self._worker_main_loop(agent, 0, agent.is_async())
logger.info(f"Single worker mode finished. Tasks processed: {num_tasks}")
# If algorithm is provided and we have datasets, run algorithm after worker completes
if self.algorithm is not None and train_dataset is not None:
logger.info("Running algorithm training after worker completion.")
self.algorithm.run(
train_dataset=train_dataset,
val_dataset=val_dataset,
)
else:
logger.info(f"Running with n_workers={self.n_workers} ({mode} multiprocessing).")
for i in range(self.n_workers):
process_name = f"AgentLightning-Worker-{i}"
p = multiprocessing.Process(
target=self._worker_main_loop,
args=(agent, i, agent.is_async()),
daemon=self.daemon,
name=process_name,
)
processes.append(p)
logger.info(f"Starting worker process {i} (name: {process_name})...")
p.start()
if self.daemon:
# If algorithm is provided and we have datasets, pass them to the algorithm
if self.algorithm is not None:
logger.info("All workers have been spawned. Running algorithm training with provided datasets.")
self.algorithm.run(
train_dataset=train_dataset,
val_dataset=val_dataset,
)
logger.info("Algorithm exits. Killing the workers.")
self._terminate_processes(processes)
for i, p in enumerate(processes):
p.join() # Wait for the process to complete
logger.info(
f"Worker process {i} (name: {p.name}, PID: {p.pid}) joined with exit code {p.exitcode}."
)
if p.exitcode != 0:
logger.warning(
f"Worker process {i} (name: {p.name}, PID: {p.pid}) exited with non-zero code: {p.exitcode}."
)
logger.info(f"All {self.n_workers} worker processes have completed.")
else:
logger.info("All worker processes started. Main process will not wait.")
# A hack to stop the main process from waiting for child processes to finish.
time.sleep(1) # Give workers time to start
import multiprocessing.process as multiprocessing_process
multiprocessing_process._children.clear() # type: ignore
if self.algorithm is not None:
logger.info("Main process continues to run algorithm.")
self.algorithm.run(
train_dataset=train_dataset,
val_dataset=val_dataset,
)
logger.info("Algorithm exits. Killing the workers.")
self._terminate_processes(processes)
except KeyboardInterrupt:
logger.info("KeyboardInterrupt received. Killing the workers.")
self._terminate_processes(processes)
logger.info(f"Workers terminated or single worker interrupted.")
raise
except Exception:
logger.exception(f"Unhandled exception in fit method.")
self._terminate_processes(processes)
logger.info(f"Workers terminated or single worker interrupted.")
raise
finally:
if self.daemon:
self.teardown()
else:
logger.info("Main process exiting. Please use Trainer.kill_orphaned_processes() for cleanup.")