Source code for tracing.decorators

import functools
import warnings
from typing import List, Optional, Dict, TYPE_CHECKING
import logging

if TYPE_CHECKING:
    from adalflow.core.generator import Generator
from adalflow.tracing import GeneratorStateLogger, GeneratorCallLogger

log = logging.getLogger(__name__)


[docs] def trace_generator_states( attributes: Optional[List[str]] = None, # list of attributes of type Generator save_dir: Optional[str] = "./traces/", project_name: Optional[str] = None, filename: Optional[str] = None, ): r"""Decorator to trace generators in a task component. It dynamically attaches a GeneratorLogger to the target generator attribute and logs the prompt states of the generator. You can use it on any component that has attributes pointing to a generator object. Args: attributes (List[str], Optional): The list of attributes that point to the generator objects. If not provided, it will automatically detect the attributes that are instances of Generator. filepath (str, Optional): The path to the directory where the trace file will be saved. Default is "./traces/". filename (str, Optional): The name of the trace file. If not provided, it will be "{class_name}_generator_trace.json". Examples: .. code-block:: python from adalflow.tracing import trace_generator_states # Define a class and apply the decorator @trace_generator_states() class TestGenerator: def __init__(self): super().__init__() prompt_kwargs = {"input_str": "world"} self.generator = Generator( model_client=OpenAIClient(), prompt_kwargs=prompt_kwargs, model_kwargs={"model": "gpt-3.5-turbo"}, ) # now you will see log files in the ./traces/ with a filename like TestGenerator_generator_trace.json # If you update the prompt templates or the prompt_kwargs, it will be logged in the file. """ def decorator(cls): original_init = cls.__init__ class_name = cls.__name__ logger_project_name = project_name or class_name # final_filename = filename or f"{class_name}_generator_trace.json" # final_file = os.path.join(filepath, final_filename) # log.info(f"Tracing generator in {class_name} to {final_file}") @functools.wraps(original_init) def new_init(self, *args, **kwargs): from adalflow.core.generator import Generator original_init(self, *args, **kwargs) # enable automatic detection of generator attributes effective_attributes = attributes or [ attr for attr in dir(self) if isinstance(getattr(self, attr, None), Generator) ] # create the logger in the current component if not hasattr(self, "generator_logger"): log.debug(f"Creating generator states logger for {class_name}") self.generator_logger = GeneratorStateLogger( save_dir=save_dir, project_name=logger_project_name, filename=filename, ) # Dynamically get the attribute to be logged if it exists. for attribute in effective_attributes: log.debug(f"Tracing generator states in {class_name}") target = getattr(self, attribute, None) generator_name = attribute if target is None: warnings.warn( f"Attribute {attribute} not found in {class_name}. Skipping tracing." ) continue if not isinstance(target, Generator): warnings.warn( f"Attribute {attribute} is not a Generator instance. Skipping tracing." ) continue # log the prompt states of the target generator self.generator_logger.log_prompt(target, generator_name) cls.__init__ = new_init return cls return decorator
[docs] def trace_generator_call( attributes: Optional[List[str]] = None, save_dir: Optional[str] = "./traces/", error_only: bool = True, ): r"""Decorator to trace generator predictions in a task component, especially failed ones. This decorator is a wrapper around the generator call method. It logs the generator call by reading its GeneratorOutput and logs the call if the output is an error. Args: attributes (List[str]): The list of attributes that point to the generator objects. save_dir (str): The path to the directory where the trace file will be saved. error_only (bool): If True, only log the calls that have an error. Default is True. Examples: .. code-block:: python from adalflow.tracing import trace_generator_call @trace_generator_call() class TestGenerator: def __init__(self): super().__init__() prompt_kwargs = {"input_str": "world"} self.generator = Generator( model_client=OpenAIClient(), prompt_kwargs=prompt_kwargs, model_kwargs={"model": "gpt-3.5-turbo"}, ) # now you will see log files in the ./traces/ with a filename like TestGenerator_generator_call.jsonl # If the generator call has an error, it will be logged in the file. If you want to decorate a component(such as LLMRetriever) from the library where you do not have access to the source code, you can do it like this: .. code-block:: python from adalflow.components.retriever import LLMRetriever # Define a subclass and apply the decorator @trace_generator_call(save_dir=...) class LoggedLLMRetriever(LLMRetriever): pass retriever = LoggedLLMRetriever(...) You can access the logger via TestGenerator.generator_call_logger if you want to access call records in the code. """ def decorator(cls): original_init = cls.__init__ class_name = cls.__name__ from adalflow.core.generator import Generator def _wrap_generator( generator_name: str, generator: "Generator", error_only: bool, logger: GeneratorCallLogger, ): r"""Wrap the call method of the generator to log the call.""" original_call = generator.call @functools.wraps(original_call) def wrapped_call(*args, **kwargs): output = original_call(*args, **kwargs) try: if (error_only and output.error is not None) or not error_only: log.debug(f"Logging generator call for {generator_name}") logger.log_call( name=generator_name, model_kwargs=kwargs.get("model_kwargs", {}), prompt_kwargs=kwargs.get("prompt_kwargs", {}), output=output, ) except Exception as e: log.error(f"Error logging generator call for {generator_name}: {e}") return output return wrapped_call @functools.wraps(original_init) def new_init(self, *args, **kwargs): original_init(self, *args, **kwargs) # Find generator attributes effective_attributes = attributes or [ attr for attr in dir(self) if isinstance(getattr(self, attr, None), Generator) ] generator_names_to_files: Dict[str, str] = {} # create the logger in the current component if not hasattr(self, "generator_call_logger"): self.generator_call_logger = GeneratorCallLogger( save_dir=save_dir, project_name=class_name ) generator_names_to_files = ( self.generator_call_logger.generator_names_to_files ) # Wrap each generator (with attr_name as the generator name) for attr_name in effective_attributes: target_generator = getattr(self, attr_name, None) if target_generator is None: warnings.warn( f"Attribute {attr_name} not found in {class_name}. Skipping tracing." ) continue # handle the file registration if attr_name not in generator_names_to_files: self.generator_call_logger.register_generator(attr_name) filename = self.generator_call_logger.get_log_location(attr_name) log.info(f"Registered generator {attr_name} with file {filename}") # Wrap the call method of the target generator if target_generator and hasattr(target_generator, "call"): setattr( target_generator, "call", _wrap_generator( generator_name=attr_name, generator=target_generator, error_only=error_only, logger=self.generator_call_logger, ), ) cls.__init__ = new_init return cls return decorator