Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f174aa07e | |||
| cc3351b0ee | |||
| 3c59a8c04e | |||
| 423837924a | |||
| 987e6a716b | |||
| 6c478aeea0 | |||
| fb33cc7e71 | |||
| 9be342f3bf | |||
| df7c66471d | |||
| a9cfbd4658 | |||
| 1b072d28f3 | |||
| 3f38f6b371 | |||
| dacf142f1b | |||
| c4f4267fba |
@@ -10,7 +10,9 @@ from .emitter import *
|
||||
from .execution import *
|
||||
from .litagent import *
|
||||
from .llm_proxy import *
|
||||
from .logging import *
|
||||
from .logging import configure_logger # deprecated # type: ignore
|
||||
from .logging import setup as setup_logging # type: ignore
|
||||
from .logging import setup_module as setup_module_logging # type: ignore
|
||||
from .runner import *
|
||||
from .server import AgentLightningServer # deprecated # type: ignore
|
||||
from .store import *
|
||||
|
||||
@@ -9,7 +9,7 @@ import asyncio
|
||||
import logging
|
||||
from typing import Iterable
|
||||
|
||||
from agentlightning.logging import configure_logger
|
||||
from agentlightning import setup_logging
|
||||
from agentlightning.store.client_server import LightningStoreServer
|
||||
from agentlightning.store.memory import InMemoryLightningStore
|
||||
|
||||
@@ -27,7 +27,7 @@ def main(argv: Iterable[str] | None = None) -> int:
|
||||
)
|
||||
args = parser.parse_args(list(argv) if argv is not None else None)
|
||||
|
||||
configure_logger()
|
||||
setup_logging()
|
||||
|
||||
store = InMemoryLightningStore()
|
||||
server = LightningStoreServer(
|
||||
|
||||
+329
-13
@@ -1,10 +1,18 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import warnings
|
||||
from logging.config import dictConfig
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
__all__ = ["configure_logger"]
|
||||
from rich.console import Console
|
||||
|
||||
__all__ = ["setup", "configure_logger", "setup_module"]
|
||||
|
||||
|
||||
def configure_logger(level: int = logging.INFO, name: str = "agentlightning") -> logging.Logger:
|
||||
@@ -15,6 +23,10 @@ def configure_logger(level: int = logging.INFO, name: str = "agentlightning") ->
|
||||
not propagate to the root logger, preventing duplicate log emission when
|
||||
applications compose multiple logging configurations.
|
||||
|
||||
!!! danger
|
||||
|
||||
This function is deprecated in favor of [`setup_logging`][agentlightning.setup_logging].
|
||||
|
||||
Args:
|
||||
level: Logging level applied both to the logger and the installed
|
||||
handler. Defaults to `logging.INFO`.
|
||||
@@ -32,23 +44,327 @@ def configure_logger(level: int = logging.INFO, name: str = "agentlightning") ->
|
||||
logger.info("agent-lightning is ready!")
|
||||
```
|
||||
"""
|
||||
warnings.warn("This function is deprecated in favor of `setup_logging`.", DeprecationWarning, stacklevel=2)
|
||||
|
||||
return setup_module(level=level, name=name, console=True, color=True, propagate=False)
|
||||
|
||||
|
||||
DEFAULT_FORMAT = "%(asctime)s [%(levelname)s] (Process-%(process)d %(name)s) %(message)s"
|
||||
DATE_FORMAT = "%H:%M:%S"
|
||||
|
||||
|
||||
def _to_level_value(lvl: int | str) -> int:
|
||||
if isinstance(lvl, int):
|
||||
return lvl
|
||||
val = getattr(logging, str(lvl).upper(), None)
|
||||
if val is None:
|
||||
raise ValueError(f"Invalid log level: {lvl}")
|
||||
return val
|
||||
|
||||
|
||||
def _ensure_file_handler(
|
||||
logger: logging.Logger,
|
||||
filename: str,
|
||||
*,
|
||||
level: int,
|
||||
formatter: Optional[logging.Formatter],
|
||||
) -> None:
|
||||
"""Attach a FileHandler to `logger` for `filename` if it doesn't already exist."""
|
||||
abspath = os.path.abspath(filename)
|
||||
|
||||
# Avoid duplicates
|
||||
for h in logger.handlers:
|
||||
if isinstance(h, logging.FileHandler) and getattr(h, "baseFilename", None) == abspath:
|
||||
return
|
||||
|
||||
# Ensure directory exists
|
||||
dirname = os.path.dirname(abspath)
|
||||
if dirname:
|
||||
os.makedirs(dirname, exist_ok=True)
|
||||
|
||||
fh = logging.FileHandler(abspath, encoding="utf-8")
|
||||
fh.setLevel(level)
|
||||
if formatter is not None:
|
||||
fh.setFormatter(formatter)
|
||||
else:
|
||||
fh.setFormatter(logging.Formatter(DEFAULT_FORMAT, DATE_FORMAT))
|
||||
|
||||
logger.addHandler(fh)
|
||||
|
||||
|
||||
def setup(
|
||||
level: int | str = "INFO",
|
||||
*,
|
||||
console: bool = True,
|
||||
color: bool | Dict[str, Any] = True,
|
||||
propagate: bool = False,
|
||||
disable_existing_loggers: bool = False,
|
||||
capture_warnings: bool = False,
|
||||
submodule_levels: Optional[dict[str, int | str]] = None,
|
||||
extra_handlers: Optional[list[logging.Handler]] = None,
|
||||
formatter: Optional[logging.Formatter] = None,
|
||||
apply_to: Optional[list[str]] = None,
|
||||
files: Optional[str | dict[str, str]] = None,
|
||||
) -> None:
|
||||
"""Configures logging for the `agentlightning` logger hierarchy.
|
||||
|
||||
This function provides a one-stop setup utility for configuring the
|
||||
`agentlightning` root logger and optionally its submodules or external
|
||||
loggers. It supports console logging, colored rich output, per-submodule
|
||||
log levels, and optional handler/formatter injection.
|
||||
|
||||
The setup is intentionally isolated: it does not modify the global root
|
||||
logger or loggers belonging to other libraries unless explicitly directed
|
||||
via `apply_to`.
|
||||
|
||||
Args:
|
||||
level:
|
||||
Logging level for the base `agentlightning` logger. Accepts either
|
||||
an integer (e.g., `logging.DEBUG`) or a string level name
|
||||
(e.g., `"INFO"`). Defaults to `"INFO"`.
|
||||
console:
|
||||
Whether to attach a console handler to the logger. Defaults to
|
||||
`True`.
|
||||
color:
|
||||
Enables rich-formatted output using `RichHandler` when `True`
|
||||
or a configuration dict. If `False`, a plain text formatter is
|
||||
used instead. Defaults to `True`.
|
||||
propagate:
|
||||
Whether `agentlightning` logs should propagate to ancestor
|
||||
loggers. Defaults to `False`.
|
||||
disable_existing_loggers:
|
||||
Passed to `logging.config.dictConfig`. If `True`, disables all
|
||||
existing configured loggers before applying this configuration.
|
||||
Defaults to `False`.
|
||||
capture_warnings:
|
||||
If `True`, redirects Python `warnings` emitted via the `warnings`
|
||||
module into the logging system. Defaults to `False`.
|
||||
submodule_levels:
|
||||
Mapping of submodule logger names to logging levels. If a specified
|
||||
submodule level is more verbose than the base level, a warning is emitted.
|
||||
extra_handlers:
|
||||
A list of user-provided handlers to attach to the `agentlightning` logger.
|
||||
Handlers are added idempotently; duplicates are not reattached.
|
||||
formatter:
|
||||
A formatter to apply to any handler under `agentlightning` that does not
|
||||
already have one assigned. Useful for customizing output without overwriting
|
||||
formatters on custom handlers.
|
||||
apply_to:
|
||||
A list of additional logger names to configure identically to
|
||||
`agentlightning` base logger. Their handlers are replaced with copies of the base
|
||||
handlers, and propagation is disabled to avoid duplicate log emission.
|
||||
files:
|
||||
If a string, attach a FileHandler to the base `agentlightning` logger.
|
||||
If a dict, for each `(logger_name, filename)` pair, attach a FileHandler
|
||||
directly to that logger.
|
||||
Each file handler should use the logger's effective level at creation.
|
||||
|
||||
Notes:
|
||||
* On Windows, this function forces UTF-8 mode in the console to prevent
|
||||
issues with rich output or special characters.
|
||||
* Submodule loggers can generate records below the handler's emission
|
||||
threshold. Whether such records appear depends on both the logger's
|
||||
level and the handler's level.
|
||||
* `apply_to` loggers inherit the same handlers but do not propagate
|
||||
upward, yielding isolated, consistent behavior.
|
||||
|
||||
Examples:
|
||||
Basic setup:
|
||||
|
||||
>>> setup()
|
||||
|
||||
Enabling debug mode with no color:
|
||||
|
||||
>>> setup(level="DEBUG", color=False)
|
||||
|
||||
Overriding specific submodule levels:
|
||||
|
||||
>>> setup(submodule_levels={"agentlightning.io": "DEBUG"})
|
||||
|
||||
Attaching an additional file handler:
|
||||
|
||||
>>> fh = logging.FileHandler("app.log")
|
||||
>>> setup(extra_handlers=[fh])
|
||||
"""
|
||||
# Ensure UTF-8 encoding on Windows consoles
|
||||
# Note: This change does not fully represent support for execution under the windown system.
|
||||
# Note: This change does not fully represent support for execution under the windows system.
|
||||
# It only fixes console printing issues caused by special characters.
|
||||
# TODO: More comprehensive Windows support may be needed in the future.
|
||||
if platform.system() == "Windows":
|
||||
os.environ["PYTHONUTF8"] = "1"
|
||||
|
||||
logger = logging.getLogger(name)
|
||||
logger.handlers.clear() # clear existing handlers
|
||||
base_logger = setup_module(
|
||||
level,
|
||||
name="agentlightning",
|
||||
console=console,
|
||||
color=color,
|
||||
propagate=propagate,
|
||||
disable_existing_loggers=disable_existing_loggers,
|
||||
)
|
||||
|
||||
# log to stdout
|
||||
handler = logging.StreamHandler()
|
||||
handler.setLevel(level)
|
||||
formatter = logging.Formatter("%(asctime)s [%(levelname)s] (Process-%(process)d %(name)s) %(message)s")
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(level)
|
||||
logger.propagate = False # prevent double logging
|
||||
return logger
|
||||
base_level_value = base_logger.level
|
||||
|
||||
# Apply user-provided formatter (only to handlers without one,
|
||||
# so we don't clobber custom extra_handlers)
|
||||
if formatter is not None:
|
||||
for h in base_logger.handlers:
|
||||
if h.formatter is None:
|
||||
h.setFormatter(formatter)
|
||||
|
||||
# Attach user-provided handler(s) if any, idempotently
|
||||
if extra_handlers:
|
||||
for h in extra_handlers:
|
||||
if h not in base_logger.handlers:
|
||||
base_logger.addHandler(h)
|
||||
|
||||
# Per-submodule levels
|
||||
if submodule_levels:
|
||||
for name, lvl in submodule_levels.items():
|
||||
sub_level = _to_level_value(lvl)
|
||||
|
||||
# Emit a warning if submodule level is lower (more verbose) than the global/base level
|
||||
if sub_level < base_level_value:
|
||||
base_logger.warning(
|
||||
"Submodule logger '%s' level %s (%s) is more verbose than base "
|
||||
"logger level %s (%s). Records below the base level may still be "
|
||||
"filtered out by handlers depending on their own levels.",
|
||||
name,
|
||||
lvl,
|
||||
sub_level,
|
||||
logging.getLevelName(base_level_value),
|
||||
base_level_value,
|
||||
)
|
||||
|
||||
# The logger will *create* records down to the logger's level, but a handler
|
||||
# with a higher level will still drop anything below its own threshold.
|
||||
# Effective emission is gated by both: record.level >= logger.level AND handler.level.
|
||||
logging.getLogger(name).setLevel(lvl)
|
||||
|
||||
# Attach file handlers if requested
|
||||
if files is not None:
|
||||
if isinstance(files, str):
|
||||
# Single file for the entire `agentlightning` hierarchy.
|
||||
_ensure_file_handler(
|
||||
logger=base_logger,
|
||||
filename=files,
|
||||
level=base_level_value,
|
||||
formatter=formatter,
|
||||
)
|
||||
else:
|
||||
# Per-logger files
|
||||
for logger_name, filename in files.items():
|
||||
lg = logging.getLogger(logger_name)
|
||||
# Use the logger's *effective* level at creation time
|
||||
effective_level = lg.getEffectiveLevel()
|
||||
_ensure_file_handler(
|
||||
logger=lg,
|
||||
filename=filename,
|
||||
level=effective_level,
|
||||
formatter=formatter,
|
||||
)
|
||||
|
||||
# Optionally apply the same handler setup to other loggers outside this module
|
||||
if apply_to:
|
||||
for name in apply_to:
|
||||
lg = logging.getLogger(name)
|
||||
# This removes any existing handlers so we don't duplicate output
|
||||
# and ensures these loggers share exactly the same handlers as base_logger.
|
||||
lg.handlers.clear()
|
||||
for h in base_logger.handlers:
|
||||
lg.addHandler(h)
|
||||
lg.setLevel(base_logger.level)
|
||||
# We've attached handlers directly to these loggers; if propagate
|
||||
# stayed True, records would bubble up to ancestor loggers and could be
|
||||
# emitted twice (here and on the parent/root). Setting False isolates them.
|
||||
lg.propagate = False
|
||||
|
||||
# Optionally capture warnings
|
||||
if capture_warnings:
|
||||
logging.captureWarnings(True)
|
||||
|
||||
|
||||
def setup_module(
|
||||
level: int | str = "INFO",
|
||||
*,
|
||||
name: str = "agentlightning",
|
||||
console: bool = True,
|
||||
color: bool | Dict[str, Any] = True,
|
||||
propagate: bool = False,
|
||||
disable_existing_loggers: bool = False,
|
||||
) -> logging.Logger:
|
||||
"""Initializes and returns the base logger for `agentlightning`.
|
||||
|
||||
This function constructs and applies a `dictConfig` configuration for the
|
||||
logger hierarchy rooted at `name`. It supports either rich console
|
||||
formatting (via `RichHandler`) or plain text formatting, based on the
|
||||
`color` argument.
|
||||
|
||||
Unlike [`setup_logging`][agentlightning.setup_logging], this function configures only a single logger namespace
|
||||
and does not attach extra handlers or submodule levels. It is primarily used
|
||||
internally by [`setup_logging`][agentlightning.setup_logging] but is also suitable for direct integration in
|
||||
custom logging workflows.
|
||||
"""
|
||||
root_cfg: Dict[str, Any] = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": disable_existing_loggers,
|
||||
"loggers": {
|
||||
name: {
|
||||
"handlers": [],
|
||||
"level": level,
|
||||
"propagate": propagate,
|
||||
}
|
||||
},
|
||||
"handlers": {},
|
||||
"formatters": {},
|
||||
}
|
||||
|
||||
# Choose formatter / handler definition
|
||||
if color is not False and console:
|
||||
# Console must be true to display colored outputs
|
||||
if isinstance(color, dict):
|
||||
rich_handler_config = color
|
||||
else:
|
||||
rich_handler_config: Dict[str, Any] = {
|
||||
"rich_tracebacks": False,
|
||||
"markup": False,
|
||||
"show_time": True,
|
||||
"show_path": True,
|
||||
}
|
||||
|
||||
if not _has_width():
|
||||
# e.g., in a CI environment.
|
||||
rich_handler_config["console"] = Console(width=200)
|
||||
|
||||
root_cfg["handlers"]["console"] = {
|
||||
"class": "rich.logging.RichHandler",
|
||||
"level": level,
|
||||
**rich_handler_config,
|
||||
}
|
||||
# RichHandler manages its own style; keep formatter None
|
||||
else:
|
||||
fmt_name = "plain"
|
||||
root_cfg["formatters"][fmt_name] = {
|
||||
"format": DEFAULT_FORMAT,
|
||||
"datefmt": DATE_FORMAT,
|
||||
}
|
||||
|
||||
if console:
|
||||
root_cfg["handlers"]["console"] = {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": level,
|
||||
"formatter": fmt_name,
|
||||
}
|
||||
|
||||
# Attach selected handlers to agentlightning
|
||||
handler_names = list(root_cfg["handlers"].keys())
|
||||
root_cfg["loggers"][name]["handlers"] = handler_names
|
||||
|
||||
# Apply dictConfig (this resets the logger handlers)
|
||||
dictConfig(root_cfg)
|
||||
|
||||
return logging.getLogger(name)
|
||||
|
||||
|
||||
def _has_width() -> bool:
|
||||
"""Automatically determine whether the terminal has a width."""
|
||||
return sys.stdout.isatty()
|
||||
|
||||
@@ -1067,6 +1067,8 @@ class LightningStoreClient(LightningStore):
|
||||
"server_address": self.server_address,
|
||||
"_retry_delays": self._retry_delays,
|
||||
"_health_retry_delays": self._health_retry_delays,
|
||||
"_request_timeout": self._request_timeout,
|
||||
"_connection_timeout": self._connection_timeout,
|
||||
}
|
||||
|
||||
def __setstate__(self, state: Dict[str, Any]):
|
||||
@@ -1080,6 +1082,8 @@ class LightningStoreClient(LightningStore):
|
||||
self._lock = threading.Lock()
|
||||
self._retry_delays = state["_retry_delays"]
|
||||
self._health_retry_delays = state["_health_retry_delays"]
|
||||
self._request_timeout = state["_request_timeout"]
|
||||
self._connection_timeout = state["_connection_timeout"]
|
||||
self._dequeue_was_successful = False
|
||||
self._dequeue_first_unsuccessful = True
|
||||
|
||||
|
||||
@@ -18,13 +18,13 @@ from flask import Flask, Response, abort, request
|
||||
from tensordict import TensorDict
|
||||
from verl import DataProto
|
||||
|
||||
from agentlightning import LLM, AgentLightningServer, NamedResources, RolloutLegacy, configure_logger
|
||||
from agentlightning import LLM, AgentLightningServer, NamedResources, RolloutLegacy, setup_logging
|
||||
from agentlightning.adapter.triplet import TracerTraceToTriplet, TraceToTripletBase
|
||||
from agentlightning.llm_proxy import LLMProxy, ModelConfig
|
||||
from agentlightning.store.base import LightningStore
|
||||
from agentlightning.types import Rollout, RolloutConfig, Task
|
||||
|
||||
configure_logger()
|
||||
setup_logging()
|
||||
|
||||
__all__ = [
|
||||
"AgentModeDaemon",
|
||||
|
||||
@@ -23,3 +23,11 @@
|
||||
## CLI Builder
|
||||
|
||||
::: agentlightning.lightning_cli
|
||||
|
||||
## Logging
|
||||
|
||||
::: agentlightning.configure_logger
|
||||
|
||||
::: agentlightning.setup_module_logging
|
||||
|
||||
::: agentlightning.setup_logging
|
||||
|
||||
@@ -181,5 +181,5 @@ async def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
agl.configure_logger()
|
||||
agl.setup_logging()
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -19,7 +19,7 @@ python apo_custom_algorithm.py runner
|
||||
from apo_custom_algorithm import apo_algorithm, apo_rollout
|
||||
from rich.console import Console
|
||||
|
||||
from agentlightning import Trainer, configure_logger
|
||||
from agentlightning import Trainer, setup_logging
|
||||
from agentlightning.algorithm import algo
|
||||
from agentlightning.store import LightningStore
|
||||
|
||||
@@ -39,6 +39,6 @@ async def apo_algorithm_usable_in_trainer(*, store: LightningStore):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
configure_logger()
|
||||
setup_logging()
|
||||
trainer = Trainer(n_workers=1, algorithm=apo_algorithm_usable_in_trainer)
|
||||
trainer.fit(apo_rollout)
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import cast
|
||||
|
||||
from apo_custom_algorithm import apo_rollout
|
||||
|
||||
from agentlightning import Trainer, configure_logger
|
||||
from agentlightning import Trainer, setup_logging
|
||||
from agentlightning.litagent import LitAgent
|
||||
from agentlightning.runner import LitAgentRunner
|
||||
from agentlightning.store import InMemoryLightningStore
|
||||
@@ -105,7 +105,7 @@ def debug_with_trainer():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
configure_logger()
|
||||
setup_logging()
|
||||
|
||||
parser = argparse.ArgumentParser(description="Debug APO with runner or trainer approach.")
|
||||
parser.add_argument(
|
||||
|
||||
@@ -12,7 +12,7 @@ from typing import Any
|
||||
import dotenv
|
||||
from openai import OpenAI
|
||||
|
||||
from agentlightning import configure_logger
|
||||
from agentlightning import setup_logging
|
||||
from agentlightning.litagent import LitAgent
|
||||
from agentlightning.trainer import Trainer
|
||||
|
||||
@@ -40,7 +40,7 @@ class SimpleAgent(LitAgent[Any]):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
configure_logger()
|
||||
setup_logging()
|
||||
dotenv.load_dotenv()
|
||||
agent = SimpleAgent()
|
||||
# Use 2 workers to simulate multiple clients
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Tuple, cast
|
||||
from openai import AsyncOpenAI
|
||||
from room_selector import RoomSelectionTask, load_room_tasks, prompt_template_baseline, room_selector
|
||||
|
||||
from agentlightning import Trainer, configure_logger
|
||||
from agentlightning import Trainer, setup_logging
|
||||
from agentlightning.adapter import TraceToMessages
|
||||
from agentlightning.algorithm.apo import APO
|
||||
from agentlightning.types import Dataset
|
||||
@@ -33,7 +33,7 @@ def setup_apo_logger(file_path: str = "apo.log") -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
configure_logger()
|
||||
setup_logging()
|
||||
setup_apo_logger()
|
||||
|
||||
openai_client = AsyncOpenAI()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from aoai_finetune import AzureOpenAIFinetune
|
||||
|
||||
from agentlightning import configure_logger
|
||||
from agentlightning import setup_logging
|
||||
|
||||
finetune_algo = AzureOpenAIFinetune(
|
||||
base_deployment_name="gpt-4.1-mini",
|
||||
@@ -12,7 +12,7 @@ finetune_algo = AzureOpenAIFinetune(
|
||||
data_filter_ratio=0.6,
|
||||
)
|
||||
|
||||
configure_logger()
|
||||
setup_logging()
|
||||
|
||||
|
||||
def test_deployment():
|
||||
|
||||
@@ -5,13 +5,13 @@ from aoai_finetune import AzureOpenAIFinetune
|
||||
from capital_agent import capital_agent
|
||||
from rich.console import Console
|
||||
|
||||
from agentlightning import TraceToMessages, Trainer, configure_logger
|
||||
from agentlightning import TraceToMessages, Trainer, setup_logging
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def main():
|
||||
configure_logger()
|
||||
setup_logging()
|
||||
finetune_algo = AzureOpenAIFinetune(
|
||||
base_deployment_name="gpt-4.1-mini",
|
||||
finetuned_deployment_name="gpt-4.1-mini-ft",
|
||||
|
||||
@@ -19,9 +19,9 @@ from autogen_ext.models.openai import OpenAIChatCompletionClient
|
||||
from autogen_ext.tools.mcp import McpWorkbench, StdioServerParams
|
||||
from eval_utils import evaluate_v0_1
|
||||
|
||||
from agentlightning import LLM, LitAgent, NamedResources, Trainer, configure_logger
|
||||
from agentlightning import LLM, LitAgent, NamedResources, Trainer, setup_logging
|
||||
|
||||
configure_logger()
|
||||
setup_logging()
|
||||
|
||||
calculator_mcp_server = StdioServerParams(command="uvx", args=["mcp-server-calculator"])
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ from agentlightning import (
|
||||
LitAgent,
|
||||
NamedResources,
|
||||
Trainer,
|
||||
configure_logger,
|
||||
setup_logging,
|
||||
)
|
||||
|
||||
configure_logger()
|
||||
setup_logging()
|
||||
|
||||
agent_prompt = """You are an assistant who answers questions using Wikipedia retriever. Answer the question using only the retrieved passages. Verify your answer directly against the text.
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ import requests
|
||||
from openai import OpenAI
|
||||
from qa_em import compute_score_em
|
||||
|
||||
from agentlightning import LLM, LitAgent, NamedResources, Trainer, configure_logger, reward
|
||||
from agentlightning import LLM, LitAgent, NamedResources, Trainer, reward, setup_logging
|
||||
|
||||
configure_logger()
|
||||
setup_logging()
|
||||
|
||||
# Copied and adapted from https://github.com/PeterGriffinJin/Search-R1/blob/main/scripts/data_process/nq_search.py
|
||||
INSTRUCTION_FORMAT = """Answer the given question. You must conduct reasoning inside <think> and </think> first every time you get new information. After reasoning, if you find you lack some knowledge, you can call a search engine by <search> query </search> and it will return the top searched results between <information> and </information>. You can search as many times as your want. If you find no further external knowledge needed, you can directly provide the answer inside <answer> and </answer>, without detailed illustrations. For example, <answer> Beijing </answer>. Question: """
|
||||
|
||||
@@ -9,6 +9,7 @@ as well as https://langchain-ai.github.io/langgraph/tutorials/sql-agent/
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@@ -29,9 +30,9 @@ from spider_eval.exec_eval import eval_exec_match
|
||||
|
||||
import agentlightning as agl
|
||||
|
||||
agl.configure_logger()
|
||||
agl.setup_logging(apply_to=[__name__])
|
||||
|
||||
logger = agl.configure_logger(name=__name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
WRITE_QUERY_PROMPT = ChatPromptTemplate(
|
||||
|
||||
@@ -192,7 +192,7 @@ def main():
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
agl.configure_logger()
|
||||
agl.setup_logging()
|
||||
if args.mode == "algo":
|
||||
run_algo()
|
||||
elif args.mode == "runner":
|
||||
|
||||
@@ -367,7 +367,7 @@ def main() -> None:
|
||||
runner_parser.set_defaults(func=_run_runner)
|
||||
|
||||
args = parser.parse_args()
|
||||
agl.configure_logger()
|
||||
agl.setup_logging()
|
||||
args.func(args)
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ It should be included in CI in future if we decided to maintain this example.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
import openai
|
||||
@@ -24,13 +23,12 @@ from agentlightning import (
|
||||
LLMProxy,
|
||||
LlmProxyTraceToTriplet,
|
||||
TracerTraceToTriplet,
|
||||
configure_logger,
|
||||
emit_reward,
|
||||
setup_logging,
|
||||
)
|
||||
from agentlightning.store import LightningStoreThreaded
|
||||
|
||||
configure_logger(name="agentlightning")
|
||||
configure_logger(name="agl_tinker", level=logging.INFO)
|
||||
setup_logging(apply_to=["agl_tinker"])
|
||||
|
||||
|
||||
async def test_tracer():
|
||||
|
||||
@@ -29,7 +29,7 @@ from openai import AsyncOpenAI
|
||||
from rich.console import Console
|
||||
from trl import SFTConfig, SFTTrainer # type: ignore
|
||||
|
||||
from agentlightning import Trainer, configure_logger
|
||||
from agentlightning import Trainer, setup_logging
|
||||
from agentlightning.litagent import rollout
|
||||
from agentlightning.types import LLM, Dataset
|
||||
|
||||
@@ -173,5 +173,5 @@ def math_agent_dry_run() -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
configure_logger()
|
||||
setup_logging()
|
||||
math_agent_dry_run()
|
||||
|
||||
@@ -32,7 +32,7 @@ from math_agent import GsmProblem, load_math_dataset
|
||||
from rich.console import Console
|
||||
from unsloth_helper import unsloth_training
|
||||
|
||||
from agentlightning import configure_logger
|
||||
from agentlightning import setup_logging
|
||||
from agentlightning.adapter import LlmProxyTraceToTriplet, TraceToTripletBase
|
||||
from agentlightning.llm_proxy import LLMProxy, ModelConfig
|
||||
from agentlightning.store import LightningStore, LightningStoreClient
|
||||
@@ -380,7 +380,7 @@ async def sft_algorithm(*, store: LightningStore) -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
configure_logger()
|
||||
setup_logging()
|
||||
|
||||
store = LightningStoreClient("http://localhost:4747")
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from math_agent import GsmProblem, load_math_dataset, math_agent
|
||||
from rich.console import Console
|
||||
from sft_algorithm import sft_one_iter
|
||||
|
||||
from agentlightning import Trainer, configure_logger
|
||||
from agentlightning import Trainer, setup_logging
|
||||
from agentlightning.adapter import TraceToTripletBase
|
||||
from agentlightning.algorithm import Algorithm
|
||||
from agentlightning.llm_proxy import LLMProxy
|
||||
@@ -94,7 +94,7 @@ class UnslothSupervisedFinetuning(Algorithm):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
configure_logger()
|
||||
setup_logging()
|
||||
|
||||
algo = UnslothSupervisedFinetuning(
|
||||
max_iterations=2,
|
||||
|
||||
@@ -17,7 +17,7 @@ import multiprocessing
|
||||
from math_agent import GsmProblem, math_agent
|
||||
from rich.console import Console
|
||||
|
||||
from agentlightning import configure_logger
|
||||
from agentlightning import setup_logging
|
||||
from agentlightning.runner import LitAgentRunner
|
||||
from agentlightning.store import LightningStore, LightningStoreClient
|
||||
from agentlightning.tracer import OtelTracer
|
||||
@@ -67,6 +67,6 @@ def spawn_runners(*, store: LightningStore, n_runners: int) -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
configure_logger()
|
||||
setup_logging()
|
||||
store = LightningStoreClient("http://localhost:4747")
|
||||
spawn_runners(store=store, n_runners=4)
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
from multiprocessing.queues import Queue
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pytest
|
||||
|
||||
from agentlightning.logging import _to_level_value # pyright: ignore[reportPrivateUsage]
|
||||
from agentlightning.logging import (
|
||||
DATE_FORMAT,
|
||||
DEFAULT_FORMAT,
|
||||
)
|
||||
|
||||
|
||||
def _logging_worker(case: str, queue: Queue[Dict[str, Any]]) -> None:
|
||||
"""
|
||||
Runs in a separate process using spawn. It performs a specific logging
|
||||
configuration scenario and returns a summary dict via the queue.
|
||||
"""
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
# Re-import inside the subprocess so everything is picklable & isolated
|
||||
from agentlightning.logging import (
|
||||
setup,
|
||||
setup_module,
|
||||
)
|
||||
|
||||
if case == "setup_module_plain_console":
|
||||
logger = setup_module(
|
||||
level="DEBUG",
|
||||
name="agentlightning.test",
|
||||
console=True,
|
||||
color=False,
|
||||
propagate=False,
|
||||
)
|
||||
|
||||
handlers = logger.handlers
|
||||
handler = handlers[0] if handlers else None
|
||||
fmt = handler.formatter._fmt if handler and handler.formatter else None
|
||||
datefmt = handler.formatter.datefmt if handler and handler.formatter else None
|
||||
|
||||
queue.put(
|
||||
{
|
||||
"logger_name": logger.name,
|
||||
"logger_level": logger.level,
|
||||
"num_handlers": len(handlers),
|
||||
"handler_class": handler.__class__.__name__ if handler else None,
|
||||
"handler_level": handler.level if handler else None,
|
||||
"fmt": fmt,
|
||||
"datefmt": datefmt,
|
||||
}
|
||||
)
|
||||
|
||||
elif case == "setup_module_color_rich":
|
||||
# Rich variant: color=True uses RichHandler
|
||||
logger = setup_module(
|
||||
level="INFO",
|
||||
name="agentlightning.rich",
|
||||
console=True,
|
||||
color=True,
|
||||
propagate=False,
|
||||
)
|
||||
handlers = logger.handlers
|
||||
handler = handlers[0] if handlers else None
|
||||
|
||||
queue.put(
|
||||
{
|
||||
"logger_name": logger.name,
|
||||
"logger_level": logger.level,
|
||||
"num_handlers": len(handlers),
|
||||
"handler_class": handler.__class__.__name__ if handler else None,
|
||||
"handler_has_formatter": handler.formatter is not None if handler else None,
|
||||
}
|
||||
)
|
||||
|
||||
elif case == "setup_with_submodules_apply_to_capture_warnings":
|
||||
# Extra handler to attach via extra_handlers
|
||||
stream = io.StringIO()
|
||||
stream_handler = logging.StreamHandler(stream)
|
||||
|
||||
setup(
|
||||
level="INFO",
|
||||
console=False,
|
||||
color=False,
|
||||
propagate=False,
|
||||
disable_existing_loggers=False,
|
||||
capture_warnings=True,
|
||||
submodule_levels={"agentlightning.io": "DEBUG"},
|
||||
extra_handlers=[stream_handler],
|
||||
apply_to=["external"],
|
||||
)
|
||||
|
||||
base = logging.getLogger("agentlightning")
|
||||
sub = logging.getLogger("agentlightning.io")
|
||||
ext = logging.getLogger("external")
|
||||
|
||||
# Capture warnings via logging after capture_warnings=True
|
||||
class ListHandler(logging.Handler):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.records: List[logging.LogRecord] = []
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
self.records.append(record)
|
||||
|
||||
lh = ListHandler()
|
||||
wlog = logging.getLogger("py.warnings")
|
||||
wlog.handlers.clear()
|
||||
wlog.addHandler(lh)
|
||||
wlog.setLevel(logging.WARNING)
|
||||
wlog.propagate = False
|
||||
|
||||
warnings.warn("from warnings", UserWarning)
|
||||
|
||||
queue.put(
|
||||
{
|
||||
"base_level": base.level,
|
||||
"base_num_handlers": len(base.handlers),
|
||||
"extra_in_base": stream_handler in base.handlers,
|
||||
"sub_level": sub.level,
|
||||
"ext_level": ext.level,
|
||||
"ext_handlers_same": base.handlers == ext.handlers,
|
||||
"ext_propagate": ext.propagate,
|
||||
"warnings_logged": len(lh.records),
|
||||
}
|
||||
)
|
||||
|
||||
elif case == "setup_with_console_and_extra_handler":
|
||||
# Console + extra handler combination to test handler attachment
|
||||
stream = io.StringIO()
|
||||
extra_handler = logging.StreamHandler(stream)
|
||||
|
||||
setup(
|
||||
level="WARNING",
|
||||
console=True,
|
||||
color=False,
|
||||
propagate=False,
|
||||
extra_handlers=[extra_handler],
|
||||
)
|
||||
|
||||
base = logging.getLogger("agentlightning")
|
||||
handler_classes = [h.__class__.__name__ for h in base.handlers]
|
||||
has_extra = extra_handler in base.handlers
|
||||
|
||||
queue.put(
|
||||
{
|
||||
"base_level": base.level,
|
||||
"num_handlers": len(base.handlers),
|
||||
"handler_classes": handler_classes,
|
||||
"has_extra": has_extra,
|
||||
}
|
||||
)
|
||||
|
||||
else:
|
||||
queue.put({})
|
||||
|
||||
|
||||
def _logging_worker_files_string(queue: Queue[Dict[str, Any]], base_dir: str) -> None:
|
||||
"""
|
||||
Runs in a separate spawned process and configures logging with a single
|
||||
files=str path. Returns information about the attached FileHandler.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from agentlightning.logging import setup
|
||||
|
||||
log_path = os.path.join(base_dir, "logs", "agent.log")
|
||||
|
||||
setup(
|
||||
level="INFO",
|
||||
console=False,
|
||||
color=False,
|
||||
propagate=False,
|
||||
files=log_path,
|
||||
)
|
||||
|
||||
base = logging.getLogger("agentlightning")
|
||||
file_handlers = [h for h in base.handlers if isinstance(h, logging.FileHandler)]
|
||||
fh = file_handlers[0] if file_handlers else None
|
||||
|
||||
fmt = fh.formatter._fmt if fh and fh.formatter else None
|
||||
datefmt = fh.formatter.datefmt if fh and fh.formatter else None
|
||||
|
||||
queue.put(
|
||||
{
|
||||
"logger_level": base.level,
|
||||
"num_handlers": len(base.handlers),
|
||||
"num_file_handlers": len(file_handlers),
|
||||
"file_base": fh.baseFilename if fh else None,
|
||||
"file_level": fh.level if fh else None,
|
||||
"fmt": fmt,
|
||||
"datefmt": datefmt,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _logging_worker_files_mapping(queue: Queue[Dict[str, Any]], base_dir: str) -> None:
|
||||
"""
|
||||
Runs in a separate spawned process and configures logging with a files=dict
|
||||
mapping, then calls setup twice to verify idempotent FileHandler attachment.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from agentlightning.logging import setup
|
||||
|
||||
base_log = os.path.join(base_dir, "agent.log")
|
||||
external_log = os.path.join(base_dir, "external.log")
|
||||
|
||||
files_mapping: Dict[str, str] = {
|
||||
"agentlightning": base_log,
|
||||
"external": external_log,
|
||||
}
|
||||
|
||||
def file_handlers(logger: logging.Logger) -> list[logging.FileHandler]:
|
||||
return [h for h in logger.handlers if isinstance(h, logging.FileHandler)]
|
||||
|
||||
# First setup call
|
||||
setup(
|
||||
level="DEBUG",
|
||||
console=False,
|
||||
color=False,
|
||||
propagate=False,
|
||||
files=files_mapping,
|
||||
)
|
||||
|
||||
base_logger = logging.getLogger("agentlightning")
|
||||
ext_logger = logging.getLogger("external")
|
||||
|
||||
base_fh_first = file_handlers(base_logger)
|
||||
ext_fh_first = file_handlers(ext_logger)
|
||||
|
||||
# Second setup call with the same mapping should not add duplicate FileHandlers
|
||||
setup(
|
||||
level="DEBUG",
|
||||
console=False,
|
||||
color=False,
|
||||
propagate=False,
|
||||
files=files_mapping,
|
||||
)
|
||||
|
||||
base_fh_second = file_handlers(base_logger)
|
||||
ext_fh_second = file_handlers(ext_logger)
|
||||
|
||||
queue.put(
|
||||
{
|
||||
"base_level": base_logger.level,
|
||||
"ext_level": ext_logger.getEffectiveLevel(),
|
||||
"base_first_count": len(base_fh_first),
|
||||
"ext_first_count": len(ext_fh_first),
|
||||
"base_second_count": len(base_fh_second),
|
||||
"ext_second_count": len(ext_fh_second),
|
||||
"base_file_first": base_fh_first[0].baseFilename if base_fh_first else None,
|
||||
"ext_file_first": ext_fh_first[0].baseFilename if ext_fh_first else None,
|
||||
"base_file_second": base_fh_second[0].baseFilename if base_fh_second else None,
|
||||
"ext_file_second": ext_fh_second[0].baseFilename if ext_fh_second else None,
|
||||
# For sanity: capture handler levels as well
|
||||
"base_handler_level": base_fh_first[0].level if base_fh_first else None,
|
||||
"ext_handler_level": ext_fh_first[0].level if ext_fh_first else None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _run_case(case: str) -> Dict[str, Any]:
|
||||
"""Helper to run a scenario in a spawn’ed process and fetch the result."""
|
||||
ctx = mp.get_context("spawn")
|
||||
q: Queue[Dict[str, Any]] = ctx.Queue()
|
||||
p = ctx.Process(target=_logging_worker, args=(case, q))
|
||||
p.start()
|
||||
result = q.get(timeout=10)
|
||||
p.join(timeout=10)
|
||||
assert p.exitcode == 0
|
||||
return result
|
||||
|
||||
|
||||
def test_to_level_value_int_and_str() -> None:
|
||||
# direct, no multiprocessing needed
|
||||
assert _to_level_value(logging.DEBUG) == logging.DEBUG
|
||||
assert _to_level_value("info") == logging.INFO
|
||||
assert _to_level_value("WARNING") == logging.WARNING
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
_to_level_value("not-a-level")
|
||||
|
||||
|
||||
def test_setup_module_plain_console_spawn() -> None:
|
||||
result = _run_case("setup_module_plain_console")
|
||||
|
||||
assert result["logger_name"] == "agentlightning.test"
|
||||
assert result["logger_level"] == logging.DEBUG
|
||||
|
||||
# Console handler with plain formatter configured
|
||||
assert result["num_handlers"] == 1
|
||||
assert result["handler_class"].endswith("StreamHandler")
|
||||
assert result["handler_level"] == logging.DEBUG
|
||||
assert result["fmt"] == DEFAULT_FORMAT
|
||||
assert result["datefmt"] == DATE_FORMAT
|
||||
|
||||
|
||||
def test_setup_module_color_rich_spawn() -> None:
|
||||
# Only run this test if rich is installed
|
||||
pytest.importorskip("rich")
|
||||
|
||||
result = _run_case("setup_module_color_rich")
|
||||
|
||||
assert result["logger_name"] == "agentlightning.rich"
|
||||
assert result["logger_level"] == logging.INFO
|
||||
assert result["num_handlers"] == 1
|
||||
# We can’t rely on full module path, just the class name
|
||||
assert result["handler_class"].endswith("RichHandler")
|
||||
|
||||
|
||||
def test_setup_with_submodules_apply_to_and_capture_warnings_spawn() -> None:
|
||||
result = _run_case("setup_with_submodules_apply_to_capture_warnings")
|
||||
|
||||
# Base logger level and handler attachment
|
||||
assert result["base_level"] == logging.INFO
|
||||
assert result["base_num_handlers"] >= 1
|
||||
assert result["extra_in_base"] is True
|
||||
|
||||
# Submodule level overridden
|
||||
assert result["sub_level"] == logging.DEBUG
|
||||
|
||||
# apply_to logger mirrors base handlers & level, propagation disabled
|
||||
assert result["ext_level"] == logging.INFO
|
||||
assert result["ext_handlers_same"] is True
|
||||
assert result["ext_propagate"] is False
|
||||
|
||||
# capture_warnings=True causes warnings.warn to go through logging
|
||||
assert result["warnings_logged"] >= 1
|
||||
|
||||
|
||||
def test_setup_with_console_and_extra_handler_spawn() -> None:
|
||||
result = _run_case("setup_with_console_and_extra_handler")
|
||||
|
||||
# Level propagated to base logger
|
||||
assert result["base_level"] == logging.WARNING
|
||||
|
||||
# Both console handler and extra handler should be attached
|
||||
assert result["num_handlers"] >= 2
|
||||
assert any(cls.endswith("StreamHandler") for cls in result["handler_classes"])
|
||||
assert result["has_extra"] is True
|
||||
|
||||
|
||||
def test_setup_files_string_spawn(tmp_path: Path) -> None:
|
||||
"""
|
||||
Verifies that passing files as a string attaches a single FileHandler with
|
||||
the expected level and default formatter in a spawned process.
|
||||
"""
|
||||
ctx = mp.get_context("spawn")
|
||||
q: Queue[Dict[str, Any]] = ctx.Queue()
|
||||
p = ctx.Process(target=_logging_worker_files_string, args=(q, str(tmp_path)))
|
||||
p.start()
|
||||
result = q.get(timeout=10)
|
||||
p.join(timeout=10)
|
||||
assert p.exitcode == 0
|
||||
|
||||
assert result["logger_level"] == logging.INFO
|
||||
# We expect at least one handler and exactly one FileHandler
|
||||
assert result["num_handlers"] >= 1
|
||||
assert result["num_file_handlers"] == 1
|
||||
|
||||
# Filename should be inside the tmp_path tree
|
||||
assert str(tmp_path) in result["file_base"]
|
||||
# FileHandler uses the base logger level
|
||||
assert result["file_level"] == logging.INFO
|
||||
|
||||
# Default formatter applied by _ensure_file_handler
|
||||
assert result["fmt"] == DEFAULT_FORMAT
|
||||
assert result["datefmt"] == DATE_FORMAT
|
||||
|
||||
|
||||
def test_setup_files_mapping_spawn(tmp_path: Path) -> None:
|
||||
"""
|
||||
Verifies that passing files as a mapping attaches FileHandlers to each
|
||||
logger and that calling setup twice does not create duplicate handlers.
|
||||
"""
|
||||
ctx = mp.get_context("spawn")
|
||||
q: Queue[Dict[str, Any]] = ctx.Queue()
|
||||
p = ctx.Process(target=_logging_worker_files_mapping, args=(q, str(tmp_path)))
|
||||
p.start()
|
||||
result = q.get(timeout=10)
|
||||
p.join(timeout=10)
|
||||
assert p.exitcode == 0
|
||||
|
||||
# Base logger level is DEBUG
|
||||
assert result["base_level"] == logging.DEBUG
|
||||
|
||||
# External's effective level is WARNING (inherited from root)
|
||||
assert result["ext_level"] == logging.WARNING
|
||||
|
||||
# First setup: one FileHandler per logger
|
||||
assert result["base_first_count"] == 1
|
||||
assert result["ext_first_count"] == 1
|
||||
|
||||
# Second setup: still one FileHandler per logger (idempotence)
|
||||
assert result["base_second_count"] == 1
|
||||
assert result["ext_second_count"] == 1
|
||||
|
||||
# File paths are stable across calls
|
||||
assert result["base_file_first"] == result["base_file_second"]
|
||||
assert result["ext_file_first"] == result["ext_file_second"]
|
||||
|
||||
# Paths should live under tmp_path
|
||||
assert str(tmp_path) in result["base_file_first"]
|
||||
assert str(tmp_path) in result["ext_file_first"]
|
||||
|
||||
# Handler levels:
|
||||
# - base handler uses the base logger level (DEBUG)
|
||||
# - external handler uses external's effective level at creation (WARNING)
|
||||
assert result["base_handler_level"] == logging.DEBUG
|
||||
assert result["ext_handler_level"] == logging.WARNING
|
||||
@@ -791,9 +791,6 @@ async def test_run_gunicorn_reports_health_failure_preload():
|
||||
"""
|
||||
Health endpoint returns 503 -> watchdog posts error and requests graceful shutdown.
|
||||
"""
|
||||
from agentlightning.logging import configure_logger
|
||||
|
||||
configure_logger(logging.DEBUG)
|
||||
host = "127.0.0.1"
|
||||
port = portpicker.pick_unused_port()
|
||||
ctx = multiprocessing.get_context("fork")
|
||||
|
||||
Reference in New Issue
Block a user