Skip to content

Action

conatus.actions.action

Action class.

ActionBlueprint

Bases: type

Metaclass for the Action base classes.

This metaclass will be called whenever an Action class (not an instance) will be defined. Note that this will be called even for subclasses at class creation time. (At class instantiation type, it's Action.__new__ that should be called.)

identify_action_function staticmethod

identify_action_function(
    name: str, namespace: dict[str, ParamType]
) -> Callable[Ps, ParamType]

Identify the action function in the class namespace.

The rules are the following: 1. If there's only one executable method defined, it's the action function. 2. Otherwise, it there's one function marked as an action function, it's the action function. 3. Otherwise, raise an error.

We use the __is_action_function attribute to identify the action function. This attribute is added by the @Action.function decorator.

Unfortunately, there's no easy way to implement this logic in the @Action.function decorator, since we're dealing at this stage with classes that haven't been fully created. So we have to wait until we are in the metaclass to do this check.

PARAMETER DESCRIPTION
name

The class name. It can't be retrieved from namespace.

TYPE: str

namespace

The class namespace

TYPE: dict[str, ParamType]

RETURNS DESCRIPTION
Callable[Ps, ParamType]

The action function.

RAISES DESCRIPTION
ActionFunctionNotFoundError

If no action function was found in the class namespace.

MultipleActionFunctionsError

If multiple action functions were found in the class namespace.

ActionFunctionInferenceFoundMultipleFunctionsError

If multiple functions were found in the class namespace, and none of them are marked as an action function.

Source code in conatus/actions/action.py
@staticmethod
def identify_action_function(
    name: str,
    namespace: dict[str, ParamType],
) -> Callable[Ps, ParamType]:
    """Identify the action function in the class namespace.

    The rules are the following:
    1. If there's only one executable method defined, it's the action
       function.
    2. Otherwise, it there's one function marked as an action function,
       it's the action function.
    3. Otherwise, raise an error.

    We use the `__is_action_function` attribute to identify the action
    function. This attribute is added by the `@Action.function` decorator.

    Unfortunately, there's no easy way to implement this logic in the
    `@Action.function` decorator, since we're dealing at this stage with
    classes that haven't been fully created. So we have to wait until
    we are in the metaclass to do this check.

    Args:
        name: The class name. It can't be retrieved from namespace.
        namespace: The class namespace

    Returns:
        The action function.

    Raises:
        ActionFunctionNotFoundError: If no action function was found in the
            class namespace.
        MultipleActionFunctionsError: If multiple action functions were
            found in the class namespace.
        ActionFunctionInferenceFoundMultipleFunctionsError: If multiple
            functions were found in the class namespace, and none of them
            are marked as an action function.
    """
    action_functions: list[Callable[Ps, ParamType]] = []
    all_functions: list[Callable[Ps, ParamType]] = []

    for value in namespace.values():
        if isinstance(value, Callable):  # type: ignore[arg-type]
            cast_value = cast("Callable[Ps, ParamType]", value)
            if is_action_function(cast_value):
                action_functions.append(cast_value)
            all_functions.append(cast_value)

    len_action_functions = len(action_functions)
    len_all_functions = len(all_functions)

    # First, only deal with action functions
    # If there's only one action function, return it
    if len_action_functions == 1:
        return action_functions[0]
    # If there's more than one action function, raise an error
    if len_action_functions > 1:
        raise MultipleActionFunctionsError(name, action_functions)

    # If there's only one function, return it
    if len_all_functions == 1:
        return all_functions[0]
    # If there's more than one function, raise an error
    if len_all_functions > 1:
        raise ActionFunctionInferenceFoundMultipleFunctionsError(
            name, all_functions
        )

    raise ActionFunctionNotFoundError(name)

process_retrievability staticmethod

process_retrievability(
    origin_type: ActionOriginType,
    origin_module: str,
    origin_qualname: str,
) -> MaybeRetrievableActionInfo

Process the retrievability of the action.

This is used to determine if the action can be retrieved in recipes. Essentially, it asks the following question: if I were to run from origin_module import origin_qualname, would it work? This is necessary because if we were to run the recipe, we would need to import the action somehow.

There are two reasons why it might not work: - The action is defined in a <locals> namespace. - The action is defined in a module that is not importable, such as the Python REPL or a Jupyter notebook.

PARAMETER DESCRIPTION
origin_type

The origin of the action.

TYPE: ActionOriginType

origin_module

The module where the action was originally defined.

TYPE: str

origin_qualname

The fully qualified name of the action as originally defined.

TYPE: str

RETURNS DESCRIPTION
MaybeRetrievableActionInfo

The retrievability info.

Source code in conatus/actions/action.py
@staticmethod
def process_retrievability(
    origin_type: ActionOriginType, origin_module: str, origin_qualname: str
) -> MaybeRetrievableActionInfo:
    """Process the retrievability of the action.

    This is used to determine if the action can be retrieved in recipes.
    Essentially, it asks the following question: if I were to run
    `from origin_module import origin_qualname`, would it work? This is
    necessary because if we were to run the recipe, we would need to import
    the action somehow.

    There are two reasons why it might not work:
    - The action is defined in a `<locals>` namespace.
    - The action is defined in a module that is not importable, such as
      the Python REPL or a Jupyter notebook.

    Args:
        origin_type: The origin of the action.
        origin_module: The module where the action was originally defined.
        origin_qualname: The fully qualified name of the action as
            originally defined.

    Returns:
        (MaybeRetrievableActionInfo): The retrievability info.
    """
    if "<locals>" in origin_qualname:
        return NonRetrievableActionInfo(
            origin_type=origin_type,
            recipe_retrievable=False,
            reason_why_not_retrievable="within_locals",
            origin_module=origin_module,
            origin_qualname=origin_qualname,
        )
    if origin_module == "__main__":
        return NonRetrievableActionInfo(
            origin_type=origin_type,
            recipe_retrievable=False,
            reason_why_not_retrievable="within_main",
            origin_module=origin_module,
            origin_qualname=origin_qualname,
        )
    return RetrievableActionInfo(
        origin_type=origin_type,
        recipe_retrievable=True,
        reason_why_not_retrievable=None,
        origin_module=origin_module,
        origin_qualname=origin_qualname,
    )

detect_action_function_in_bases staticmethod

detect_action_function_in_bases(
    bases: tuple[type, ...],
) -> bool

Check if an action function is present in the base classes.

To be clear, the check is very crude: we just look for the presence of the _action_function attribute in the base classes.

PARAMETER DESCRIPTION
bases

Base classes of the class being created. Does not need to be only Action subclasses.

TYPE: tuple[type, ...]

RETURNS DESCRIPTION
bool

True if an action function is found in the base classes, False otherwise.

Source code in conatus/actions/action.py
@staticmethod
def detect_action_function_in_bases(bases: tuple[type, ...]) -> bool:
    """Check if an action function is present in the base classes.

    To be clear, the check is very crude: we just look for the presence
    of the `_action_function` attribute in the base classes.

    Args:
        bases: Base classes of the class being created. Does not need
            to be only `Action` subclasses.

    Returns:
        True if an action function is found in the base classes, False
            otherwise.
    """
    return any(hasattr(base, "_action_function") for base in bases)

__new__

__new__(
    name: str,
    bases: tuple[type, ...],
    namespace: dict[str, ParamType],
) -> type

Create a new Action class or subclass.

As part of that process, the action function is identified and stored in the class dictionary, unless the class is a base class.

PARAMETER DESCRIPTION
name

Name of the class

TYPE: str

bases

Base classes

TYPE: tuple[type, ...]

namespace

Class dictionary

TYPE: dict[str, ParamType]

RETURNS DESCRIPTION
type

New Action class

Source code in conatus/actions/action.py
def __new__(
    cls,
    name: str,
    bases: tuple[type, ...],
    namespace: dict[str, ParamType],
) -> type:
    """Create a new Action class or subclass.

    As part of that process, the action function is identified and stored
    in the class dictionary, unless the class is a base class.

    Args:
        name: Name of the class
        bases: Base classes
        namespace: Class dictionary

    Returns:
        New Action class
    """
    origin_module = str(namespace.get("__module__")) or ""
    origin_qualname = str(namespace.get("__qualname__")) or ""
    retrievability_info = cls.process_retrievability(
        origin_type="subclass",
        origin_module=origin_module,
        origin_qualname=origin_qualname,
    )
    namespace["retrievability_info"] = retrievability_info
    namespace["_intercept_setattr"] = True
    namespace["_changes"] = namespace.get("_changes", {})
    # It's OK for a base class not to have an action function
    if name in ACTION_BASE_CLASSES:
        return super().__new__(cls, name, bases, namespace)
    # Skip all the validation steps if the action function is already there
    has_af_already = namespace.get("action_function") is not None
    inherits_action_function = cls.detect_action_function_in_bases(bases)
    if has_af_already or inherits_action_function:
        return super().__new__(cls, name, bases, namespace)

    func = cls.identify_action_function(name, namespace)  # type: ignore[var-annotated]
    namespace["_action_function"] = func
    override_th_raw = namespace.get("override_type_hint_for_llm")
    override_th = isinstance(override_th_raw, bool) and override_th_raw
    # Normally, this should defer to None if desc == ""
    desc: str | None = (
        str(raw_desc) if (raw_desc := namespace.get("desc")) else None
    )
    function_info = extract_function_info(
        func,
        is_action_function=True,
        override_type_hint_for_llm=override_th,
        desc=desc,
    )
    action_name = cast("str", namespace.get("name", to_snake_case(name)))
    if not namespace.get("_privileged"):
        check_name_is_not_reserved(action_name, kind="action")
    namespace["_name"] = action_name
    function_info.fn_name = action_name
    namespace["_function_info"] = function_info
    namespace["always_available"], namespace["discriminator_keys"] = (
        determine_always_available(function_info)
    )
    pyd_models = generate_pydantic_models(
        function_info=function_info,
        use_test_variables_if_none=True,
    )
    namespace["_pydantic_models"] = pyd_models
    namespace["passive"] = namespace.get("passive", False)
    namespace["runtime_state"] = namespace.get("runtime_state")
    namespace["hide_if_computer_use_mode"] = namespace.get(
        "hide_if_computer_use_mode", False
    )
    force_update_on = namespace.get("force_update_on")
    namespace["force_update_on"] = (
        [force_update_on]
        if isinstance(force_update_on, str)
        else force_update_on
        if force_update_on is not None
        else []
    )

    if namespace.get("strict_mode") is None:
        try:
            _ = generate_openai_json_schema(
                pyd_models.json_schema,
                strict_mode=True,
            )
        except OpenAIStrictCastError as e:
            logger.debug(e)
            logger.warning(
                f"{getattr(func, '__name__', '<no name found>')}"
                " cannot be represented in "
                "OpenAI's strict mode, so it will be passed to OpenAI "
                "in non-strict mode. For more information, set the "
                "`strict_mode` keyword to `True` and read the errors."
            )
            namespace["strict_mode"] = False
        else:
            namespace["strict_mode"] = True
    else:
        namespace["strict_mode"] = namespace.get("strict_mode")
    # <> generate_pydantic_models(namespace["_function_info"])
    # <> generate_llm_friendly(namespace["_function_info"])
    # TODO(lemeb): Finish this, which means
    # 1. Writing the Pydantic schema for input + output
    # 2. The LLM-friendly version
    # 3. Testing that the I and O are correct with the inputs (thx pydantic)
    # 4. with_config / with_runtime
    # 6. We can move on to better things...
    # CTUS-40

    # Here, super().__new__(*args) is equivalent to type(*args)
    return super().__new__(cls, name, bases, namespace)

WithConfigDescriptor

WithConfigDescriptor(config_function: Callable[Ps, None])

Bases: Generic[Ps]

Descriptor to set configuration attributes on the class.

This is not meant to be used directly, but through the with_config attribute of the Action class.

Source code in conatus/actions/action.py
def __init__(
    self,
    config_function: Callable[Ps, None],
) -> None:
    """Initialize the descriptor and slurp the paramspec."""
    _ = config_function

config_set staticmethod

config_set(
    instance_or_cls: type[Y] | Y, **kwargs: ParamType
) -> None

Set configuration attributes on the class or instance.

PARAMETER DESCRIPTION
instance_or_cls

The class or instance

TYPE: type[Y] | Y

**kwargs

Configuration attributes to set on the class or instance

TYPE: ParamType DEFAULT: {}

Source code in conatus/actions/action.py
@staticmethod
def config_set(instance_or_cls: type[Y] | Y, **kwargs: ParamType) -> None:
    """Set configuration attributes on the class or instance.

    Args:
        instance_or_cls: The class or instance
        **kwargs: Configuration attributes to set on the class or instance
    """
    for k, v in kwargs.items():
        setattr(instance_or_cls, k, v)
        instance_or_cls._changes[k] = v

__get__

__get__(
    instance: None, owner: type[Y]
) -> Callable[Ps, type[Y]]
__get__(instance: Y, owner: type[Y]) -> Callable[Ps, Y]
__get__(
    instance: Y | None, owner: type[Y]
) -> Callable[Ps, Y | type[Y]]

Get the configuration attributes of the class.

This method is called when the with_config attribute is accessed on the class or on an instance of the class. It returns a function that will set the configuration attributes on the class or on the instance.

Note that the returned function does not look at the args at all. You need to pass the configuration attributes as keyword arguments.

PARAMETER DESCRIPTION
instance

The instance of the class, or None if accessed on the class.

TYPE: Y | None

owner

The class itself.

TYPE: type[Y]

RETURNS DESCRIPTION
Callable[Ps, Y | type[Y]]

A function that will set the configuration attributes on the

Callable[Ps, Y | type[Y]]

class or on the instance.

Source code in conatus/actions/action.py
def __get__(
    self, instance: Y | None, owner: type[Y]
) -> Callable[Ps, Y | type[Y]]:
    """Get the configuration attributes of the class.

    This method is called when the `with_config` attribute is accessed
    on the class or on an instance of the class. It returns
    a function that will set the configuration attributes on the class
    or on the instance.

    Note that the returned function does not look at the `args` at all.
    You need to pass the configuration attributes as keyword arguments.

    Args:
        instance: The instance of the class, or None if
            accessed on the class.
        owner: The class itself.

    Returns:
        A function that will set the configuration attributes on the
        class or on the instance.
    """

    # TODO(lemeb): Make ActionConfig an actual typed parameter to
    # constrain user behavior
    # CTUS-43
    # Returned when accessed via class: Action.with_config(...)
    def class_with_config(*_: Ps.args, **kwargs: Ps.kwargs) -> type[Y]:
        if "inplace" in kwargs and kwargs["inplace"] is True:
            __ = kwargs.pop("inplace")
            self.config_set(owner, **kwargs)
            return owner

        new_cls = cast(
            "type[Y]",
            type(
                owner.__name__,
                (owner,),
                {**owner.__dict__, "__qualname__": owner.__qualname__},
            ),
        )
        # Removing the pointer to the original class's '_changes'
        # dictionary
        new_cls._changes = new_cls._changes.copy()
        # We manually set the `_changes` dictionary because it doesn't
        # seem like `setattr` triggers `Action.__setattr__`.
        self.config_set(new_cls, **kwargs)
        return new_cls

    if instance is None:
        return class_with_config

    # Returned when accessed via instance: Action().with_config(...)
    def instance_with_config(*_: Ps.args, **kwargs: Ps.kwargs) -> Y:
        if "inplace" not in kwargs or kwargs["inplace"] is False:
            instance_or_instance_copy = copy.deepcopy(instance)
        else:
            instance_or_instance_copy = instance
        # Removing the pointer to the original class's '_changes'
        # dictionary.
        instance_or_instance_copy._changes = (
            instance_or_instance_copy._changes.copy()
        )
        self.config_set(instance_or_instance_copy, **kwargs)
        return instance_or_instance_copy

    return instance_with_config

Action

Base class for actions.

Warning

This class should not be instantiated directly. Instead, you should create a subclass of Action and define an action function within it.

Private attributes

ATTRIBUTE DESCRIPTION
_action_function

The function that the action will call. This function is created automatically if a method in a subclass is marked with the @Action.function decorator, or if an Action class is created through the @action decorator. This is a private attribute and should not be accessed directly.

TYPE: Callable[..., ParamType]

retrievability_info instance-attribute

Information about the retrievability of the action.

If the action is retrievable: - this attribute is a RetrievableActionInfo instance. - the information enables you to retrieve the original action or function when recipes are executed. - you can use the get_retrievability_instructions method to get the instructions needed to retrieve the action in a new session.

If the action is not retrievable: - this attribute is a NonRetrievableActionInfo instance. - the information enables you to understand why the action is not retrievable.

This is set automatically by the ActionBlueprint metaclass.

always_available instance-attribute

always_available: bool

If True, the action is always available to the agent.

We determine this automatically by checking if all the parameters of the action are either JSON-serializable or optional.

Warning

Please do not set this attribute manually. If you do so, you risk breaking the agent's behavior.

discriminator_keys instance-attribute

discriminator_keys: list[str]

Keys of the discriminator parameters

If not empty, the action will be available only if the agent has access to variables compatible with the discriminator parameters.

with_config class-attribute instance-attribute

with_config = WithConfigDescriptor(_config_args)

Descriptor to set configuration attributes on the class.

How to use with_config

This meant to be used like a function, and it allows you to set configuration attributes on the class.

You can use it on the class itself, or on an instance of the class:

from conatus.actions.action import Action, action_function

class MyAction(Action):

    desc = "Old description"

    @Action.function
    def my_action(self, a: int, b: int) -> int:
        return a + b

# Set the configuration attribute on the class
new_action_class = MyAction.with_config(desc="New description")
assert new_action_class.desc == "New description"

# Set the configuration attribute on an instance of the class
instance = MyAction(direct_execute=False)
new_instance = instance.with_config(desc="New description")
assert new_instance.desc == "New description"
Why is with_config an attribute?

We use an attribute here because we want to be able to set configuration attributes on the class or on an instance of the class. In situations like this (where there needs to be a single call that works on both the class and the instance), a common pattern is to use a descriptor. Descriptors essentially hijack attribute access and allow you to define custom behavior when an attribute is accessed.

You can, for now, set any attribute you want on the class. We might restrict that functionality in the future, in order to make the behavior of with_config, and action configuration in general, more predictable.

Known issues

One known issue with with_config is that its return type is either Action or TypedAction, depending on what is passed to it. This means that type checkers will not recognize your custom subclasses. This is not a problem if you don't define custom attributes in the class, but it you do, and try to access these attributes on your subclass or an instance thereof, the type checker might report attribute access issues. One work around is to override the with_config attribute in your subclass, like so:

from conatus.actions.action import Action, WithConfigDescriptor

class MyAction(Action):
    with_config: WithConfigDescriptor["MyAction"] = (
        WithConfigDescriptor["MyAction"]()
    )
PARAMETER DESCRIPTION
inplace

If True, the configuration attributes will be set on the class or instance directly, rather than creating a new class or instance. Discouraged. Defaults to False.

TYPE: bool

**kwargs

Configuration attributes to set on the class. For now, only the following attributes are supported: - desc: The description of the action. - inplace: Whether to set the configuration attributes on the class or instance directly, rather than creating a new class or instance. - strict_mode: Whether to use strict mode when passing the action to the API. - override_type_hint_for_llm: Whether to override the type hint for the LLM.

TYPE: Any

RETURNS DESCRIPTION
Action as class or instance

An instance of the class with the configuration attributes set, or the class itself if called on the class.

override_type_hint_for_llm class-attribute instance-attribute

override_type_hint_for_llm: bool = False

If True, the type hints of the function will be ignored and replaced by a more LLM-friendly version.

desc class-attribute instance-attribute

desc: str = ''

A small, LLM-friendly description of the action.

strict_mode instance-attribute

strict_mode: bool

Whether to use strict mode when passing the action to the API.

Mostly relevant for OpenAI.

passive class-attribute instance-attribute

passive: bool = False

Whether the action is passive.

If True, using the action during a run will not add it to the Recipe. In other words, it's an action that is useful for the LLM to understand the state of the world, but useless for the runtime to execute. One characteristic of passive actions is that they cannot return anything; the only thing they can do is (optionally) print to the screen.

hide_if_computer_use_mode class-attribute instance-attribute

hide_if_computer_use_mode: bool = False

Hide this action if the AI uses computer use mode.

This is meant so not as to confuse the AI by offering actions that conflict with the native capabilities of the computer use mode.

is_termination_action class-attribute instance-attribute

is_termination_action: bool = False

Flag to indicate if the action is a termination action.

This is used to indicate that the action is a termination action, i.e. an action that generally doesn't execute anything, but rather signals the end of the conversation.

runtime_state instance-attribute

runtime_state: RuntimeState | None

Pointer to the runtime state, if available.

force_update_on instance-attribute

force_update_on: list[str]

Names of the input variables that will be checked for changes.

function_info property

function_info: FunctionInfo

The function info of the action.

from conatus import action

@action
def double(x: int = 0) -> int:
    """Double the input.

    Args:
        x: The input to double.

    Returns:
        The double of the input.
    """
    return x * 2

print(double.function_info.pretty_print())
# Function info:
# - Name: 'double'
# - Description: 'Double the input.'
# - Parameter x:
#     - Description: The input to double.
#     - Type hint: <class 'int'> (JSON OK)
#     -- JSON-serializable subtype: <class 'int'>
#     -- Type of type hint: <class 'type'>
#     - Type hint shown to the LLM: 'int'
#     - Required: True
#     - Kind of parameter: POSITIONAL_OR_KEYWORD (1)
#     - Default value: 0
# - Returns:
#     - Description: The double of the input.
#     - Type hint: <class 'int'>
#     -- Type of type hint: <class 'type'>
#     - Type hint shown to the LLM: 'int'

pydantic_models property

pydantic_models: FunctionPydanticModels

The pydantic models of the action.

The object returned is a FunctionPydanticModels instance. From that, you can retrieve the following attributes:

  • input_model : The Pydantic model for the parameters of the action.
  • output_model : The Pydantic model for the output of the action.
  • json_schema : The Pydantic model for the JSON schema of the action.
  • all_fields : A dictionary containing all the fields of the action, with their name as the key and a tuple of the type and the FieldInfo instance as the value. This is mostly for internal use.

name property writable

name: str

The name of the action function.

It is automatically set at class creation time: - If the class is created through the @action decorator, the name will be the name of the function that was decorated. - If the class is created through the Action class, the name will be the snake case version of the class name.

It can also be set manually, as long as it is not in the list of reserved action names.

openai_schema property

openai_schema: FunctionDefinition

The OpenAI schema of the action function.

json_schema property

json_schema: type[BaseModel]

The Pydantic model of the JSON schema of the action.

To print it more fully, you can use Pydantic's model_json_schema method:

from conatus import action

@action
def my_action(x: int) -> int:
    return x

print(my_action.json_schema.model_json_schema())

input_model property

input_model: type[BaseModel]

The Pydantic model to validate the input arguments of the action.

function classmethod

function() -> Callable[[F], F]
function(func: None) -> Callable[[F], F]
function(func: F) -> F
function(func: F | None = None) -> F | Callable[[F], F]

Decorator to specify an action function within an Action subclass.

This method is not meant to be called directly. This only adds a bit of metadata to the function so that the class can later find it and properly execute it.

from conatus.actions.action import Action

class MyAction(Action):

    attr0 = 999

    def useful_method(self, a: int) -> int:
        return a + self.attr0

    # Now we know this is the action function
    @Action.function
    def my_action(self, a: int, b: int) -> int:
        return self.useful_method(a) + b

MyAction(1, 2)  # This will work and return 1002

Sync functions only

This decorator is meant to be used only with sync functions. We'll support async functions in the future.

One of the effects of this decorator is that it will essentially ensure that the self argument is removed from the schema we build out of the signature and docstring of the function. This is fine 99.9% of the times, but if you have defined a static method whose first argument is somehow named self, you are going to get unexpected behavior.

PARAMETER DESCRIPTION
func

The function that the action will call.

TYPE: F | None DEFAULT: None

RETURNS DESCRIPTION
F | Callable[[F], F]

The same function that was passed as an argument.

Source code in conatus/actions/action.py
@classmethod
def function(cls, func: F | None = None) -> F | Callable[[F], F]:
    """Decorator to specify an action function within an `Action` subclass.

    This method is not meant to be called directly. This only adds a bit of
    metadata to the function so that the class can later find it and
    properly execute it.

    ```python
    from conatus.actions.action import Action

    class MyAction(Action):

        attr0 = 999

        def useful_method(self, a: int) -> int:
            return a + self.attr0

        # Now we know this is the action function
        @Action.function
        def my_action(self, a: int, b: int) -> int:
            return self.useful_method(a) + b

    MyAction(1, 2)  # This will work and return 1002
    ```

    !!! note "Sync functions only"

        This decorator is meant to be used only with sync functions. We'll
        support async functions in the future.

    One of the effects of this decorator is that it will essentially ensure
    that the self argument is removed from the schema we build out of the
    signature and docstring of the function. This is fine 99.9% of the
    times, but if you have defined a static method whose first argument is
    somehow named self, you are going to get unexpected behavior.

    Args:
        func: The function that the action will call.

    Returns:
        The same function that was passed as an argument.
    """
    # NOTE: If we end up allowing users to pass arguments to this decorator,
    # we'll have to use a wrapper function here.
    if func is None:
        return mark_action_function
    return mark_action_function(func)

is_action classmethod

is_action(
    action: Self | ActionBlueprint | FunctionType,
) -> Literal["class", "instance", "none"]

Check if the action is an instance or class of Action.

RETURNS DESCRIPTION
Literal['class', 'instance', 'none']

"class" if the action is a class of Action, "instance" if it is an instance of Action, and "none" if it is neither.

Source code in conatus/actions/action.py
@classmethod
def is_action(
    cls, action: Self | ActionBlueprint | FunctionType
) -> Literal["class", "instance", "none"]:
    """Check if the action is an instance or class of `Action`.

    Returns:
        "class" if the action is a class of `Action`, "instance" if it is
            an instance of `Action`, and "none" if it is neither.
    """
    if isinstance(action, Action):
        return "instance"
    if isinstance(action, type) and issubclass(action, Action):
        return "class"
    return "none"

__deepcopy__

__deepcopy__(memo: dict[int, Action]) -> Self

Deepcopy the action.

This method is called when a deep copy of the action is made.

PARAMETER DESCRIPTION
memo

The memo dictionary.

TYPE: dict[int, Action]

RETURNS DESCRIPTION
Self

The deep copy of the action.

Source code in conatus/actions/action.py
def __deepcopy__(self: Self, memo: dict[int, Action]) -> Self:
    """Deepcopy the action.

    This method is called when a deep copy of the action is made.

    Args:
        memo: The memo dictionary.

    Returns:
        The deep copy of the action.
    """
    cls = self.__class__  # Extract the class of the object
    result = cls.__new__(cls, direct_execute=False)
    memo[id(self)] = result
    for k, v in self.__dict__.items():  # pyright: ignore[reportAny]
        setattr(
            result,
            k,
            copy.deepcopy(v, memo),  # pyright: ignore[reportAny]
        )
    return result

__new__

__new__(
    *args: ParamType,
    direct_execute: Literal[False],
    **kwargs: ParamType
) -> Self
__new__(
    *args: ParamType,
    direct_execute: Literal[True],
    **kwargs: ParamType
) -> ParamType
__new__(
    *args: ParamType,
    direct_execute: bool = True,
    **kwargs: ParamType
) -> ParamType
__new__(
    *args: ParamType,
    direct_execute: bool = True,
    **kwargs: ParamType
) -> ParamType | Self

Create a new Action instance.

This method is called when a new instance of an Action class is created. It will call the action function with the arguments passed and return the result, or the instance itself if direct_execute is set to False.

PARAMETER DESCRIPTION
*args

Positional arguments passed to the action function.

TYPE: ParamType DEFAULT: ()

direct_execute

If True, the action function will be called with the arguments passed to the constructor. If False, the instance will be returned.

TYPE: bool DEFAULT: True

**kwargs

Keyword arguments passed to the action function.

TYPE: ParamType DEFAULT: {}

RETURNS DESCRIPTION
ParamType | Self

The result of the action function if direct_execute is True, or

ParamType | Self

the instance itself otherwise.

Source code in conatus/actions/action.py
def __new__(  # type: ignore[misc]
    cls,
    *args: ParamType,
    direct_execute: bool = True,
    **kwargs: ParamType,
) -> ParamType | Self:
    """Create a new Action instance.

    This method is called when a new instance of an `Action` class is
    created. It will call the action function with the arguments passed and
    return the result, or the instance itself if `direct_execute` is set to
    False.

    Args:
        *args: Positional arguments passed to the action function.
        direct_execute: If True, the action function will be called with
            the arguments passed to the constructor. If False, the instance
            will be returned.
        **kwargs: Keyword arguments passed to the action function.

    Returns:
        The result of the action function if `direct_execute` is True, or
        the instance itself otherwise.
    """
    # Equivalent to calling `object.__new__(cls)`
    instance = super().__new__(cls)
    instance.__init__(*args, **kwargs)  # type: ignore[misc]
    if direct_execute:
        return cls._execute(instance, *args, **kwargs)
    return cast("Action", instance)

__call__

__call__(
    *args: ParamType, **kwargs: ParamType
) -> ParamType

Call the action function.

This is a convenience method that allows you to call the action function directly on the instance of the Action class.

This method does not check whether the underlying action function is async or not; you have to deal with that yourself.

from conatus.actions.action import action

@action
async def async_action(x: int) -> int:
    return x

x = 10

assert await async_action(x) == x

@action
def sync_action_2(x: int) -> int:
    return x

assert sync_action_2(x) == x
PARAMETER DESCRIPTION
*args

Positional arguments passed to the action function.

TYPE: ParamType DEFAULT: ()

**kwargs

Keyword arguments passed to the action function.

TYPE: ParamType DEFAULT: {}

RETURNS DESCRIPTION
ParamType

The result of the action function.

Source code in conatus/actions/action.py
def __call__(self, *args: ParamType, **kwargs: ParamType) -> ParamType:
    """Call the action function.

    This is a convenience method that allows you to call the action
    function directly on the instance of the `Action` class.

    This method does not check whether the underlying action function is
    async or not; you have to deal with that yourself.

    ```python
    from conatus.actions.action import action

    @action
    async def async_action(x: int) -> int:
        return x

    x = 10

    assert await async_action(x) == x

    @action
    def sync_action_2(x: int) -> int:
        return x

    assert sync_action_2(x) == x
    ```

    Args:
        *args: Positional arguments passed to the action function.
        **kwargs: Keyword arguments passed to the action function.

    Returns:
        The result of the action function.
    """
    return type(self)._execute(self, *args, **kwargs)

__setattr__

__setattr__(name: str, value: ParamType) -> None

Set an attribute on the instance.

We use this function to intercept attribute changes.

PARAMETER DESCRIPTION
name

The name of the attribute.

TYPE: str

value

The value of the attribute.

TYPE: ParamType

Source code in conatus/actions/action.py
@override
def __setattr__(self, name: str, value: ParamType) -> None:
    """Set an attribute on the instance.

    We use this function to intercept attribute changes.

    Args:
        name: The name of the attribute.
        value: The value of the attribute.
    """
    skip_intercept_for = [
        "_intercept_setattr",
        "_changes",
        "retrievability_info",
        "__orig_class__",
        "_action_function",
        "_function_info",
        "_pydantic_models",
        "runtime_state",
    ]
    if self._intercept_setattr and name not in skip_intercept_for:
        if not is_json_serializable_value(value) and isinstance(
            self.retrievability_info, RetrievableActionInfo
        ):
            self.retrievability_info = (
                self.retrievability_info.change_to_non_retrievable(
                    "changes_not_json_serializable"
                )
            )
        self._changes[name] = value
    super().__setattr__(name, value)

execute

execute(*args: ParamType, **kwargs: ParamType) -> ParamType

Explicit execution of the action function.

This is useful if you've created an instance of an Action class with direct_execute=False and you want to execute the action function afterwards. This is also a way to make type checkers happy.

from conatus.actions.action import Action

class MyAction(Action):

    @Action.function
    def my_action(self, a: int, b: int) -> int:
        return a + b

assert MyAction(1, 2) == 3  # This will work like a function call...

# ... unless you want to separate it in two steps
instance = MyAction(direct_execute=False)
assert type(instance) == MyAction
assert instance.execute(1, 2) == 3
PARAMETER DESCRIPTION
*args

Positional arguments passed to the action function.

TYPE: ParamType DEFAULT: ()

**kwargs

Keyword arguments passed to the action function.

TYPE: ParamType DEFAULT: {}

RETURNS DESCRIPTION
ParamType

The result of the action function.

Source code in conatus/actions/action.py
def execute(self, *args: ParamType, **kwargs: ParamType) -> ParamType:
    """Explicit execution of the action function.

    This is useful if you've created an instance of an `Action` class with
    `direct_execute=False` and you want to execute the action function
    afterwards. This is also a way to make type checkers happy.

    ```python
    from conatus.actions.action import Action

    class MyAction(Action):

        @Action.function
        def my_action(self, a: int, b: int) -> int:
            return a + b

    assert MyAction(1, 2) == 3  # This will work like a function call...

    # ... unless you want to separate it in two steps
    instance = MyAction(direct_execute=False)
    assert type(instance) == MyAction
    assert instance.execute(1, 2) == 3
    ```

    Args:
        *args: Positional arguments passed to the action function.
        **kwargs: Keyword arguments passed to the action function.

    Returns:
        The result of the action function.
    """
    return self._execute(*args, **kwargs)

get_openai_schema

get_openai_schema(
    *, strict_mode: bool
) -> FunctionDefinition

The OpenAI schema of the action function.

This method is meant to be used if you want to explicitly pass the strict_mode argument to the get_openai_schema method. Otherwise, you should use the openai_schema property. This is why strict_mode is a required argument.

PARAMETER DESCRIPTION
strict_mode

If True, the schema will be generated in strict mode. If False, the schema will be generated in non-strict mode.

TYPE: bool

RETURNS DESCRIPTION
FunctionDefinition

The OpenAI schema of the action function.

Source code in conatus/actions/action.py
def get_openai_schema(self, *, strict_mode: bool) -> FunctionDefinition:
    """The OpenAI schema of the action function.

    This method is meant to be used if you want to explicitly pass the
    `strict_mode` argument to the `get_openai_schema` method. Otherwise,
    you should use the `openai_schema` property. This is why `strict_mode`
    is a required argument.

    Args:
        strict_mode: If True, the schema will be generated in strict mode.
            If False, the schema will be generated in non-strict mode.

    Returns:
        The OpenAI schema of the action function.
    """
    return generate_openai_json_schema(
        self.json_schema,
        strict_mode=strict_mode,
    )

pretty_function_info

pretty_function_info() -> None

Pretty-print the function information.

Source code in conatus/actions/action.py
def pretty_function_info(self) -> None:  # pragma: no cover
    """Pretty-print the function information."""
    print(self._function_info.pretty_print(colors=True))  # noqa: T201

get_maybe_recipe_retrievability_warnings

get_maybe_recipe_retrievability_warnings(
    i: int, *, with_first_message: bool = True
) -> str | None

Generate a warning message if the action is not retrievable.

Useful when are creating an Agent and want to display the warnings then.

PARAMETER DESCRIPTION
i

The index of the action in the list of actions

TYPE: int

with_first_message

If True, the first message will be included. Defaults to True.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
str | None

The warning message if the action is not retrievable, or None

str | None

otherwise.

Source code in conatus/actions/action.py
def get_maybe_recipe_retrievability_warnings(
    self,
    i: int,
    *,
    with_first_message: bool = True,
) -> str | None:
    """Generate a warning message if the action is not retrievable.

    Useful when are creating an [`Agent`][conatus.agents.Agent]
    and want to display the warnings then.

    Args:
        i: The index of the action in the list of actions
        with_first_message: If True, the first message will be included.
            Defaults to True.

    Returns:
        The warning message if the action is not retrievable, or None
        otherwise.
    """
    msg = (
        (
            "\n⚠️IMPORTANT: You can run the agent, but you will NOT be"
            " able to write a recipe with it.\n"
            "This is because you have at least one action that cannot"
            " be retrieved:\n\n"
        )
        if with_first_message
        else ""
    )
    if isinstance(self.retrievability_info, NonRetrievableActionInfo):
        msg += f"- {self.retrievability_info.origin_qualname} "
        msg += f"({self._to_ith(i)} in your list)\n"
        match self.retrievability_info.reason_why_not_retrievable:
            case "within_locals":
                msg += (
                    "  It is within the locals of a function. \n"
                    "  You will not be able to write a recipe with it "
                    "unless you move it outside of that function."
                )
            case "within_main":  # pragma: no cover
                msg += (
                    "  It is within the main block of a script. \n"
                    "  You will not be able to write a recipe with it "
                    "unless you move it to a file."
                )
            case "changes_not_json_serializable":  # pragma: no branch
                msg += (
                    "  Some of its attributes have changed, and "
                    "these new attributes are not JSON serializable.\n"
                    "  You will not be "
                    "able to write a recipe with it unless you: \n"
                    "    1. Do not perform any changes to the action's "
                    "attributes before passing it to the agent, \n"
                    "    2. If you do need to perform changes,"
                    " make sure "
                    "that the new values are JSON serializable."
                )
        return msg
    return None

get_maybe_recipe_retrievability_instructions

get_maybe_recipe_retrievability_instructions() -> (
    RetrievabilityInstructions | None
)

Get the retrievability instructions for the action.

If the action is not retrievable, returns None.

In general, there should be no surprises with this method. If you have established that the action is retrievable, (by asserting that action.retrievability_info is an instance of RetrievableActionInfo ), then you should be sure that this method will the instructions. Nevertheless, if the JSON serialization of the changes fails, we will raise a TypeError.

RETURNS DESCRIPTION
RetrievabilityInstructions | None

The retrievability instructions for the action, or None if the

RetrievabilityInstructions | None

action is not retrievable.

RAISES DESCRIPTION
TypeError

If the changes are not JSON serializable.

Source code in conatus/actions/action.py
def get_maybe_recipe_retrievability_instructions(
    self,
) -> RetrievabilityInstructions | None:
    """Get the retrievability instructions for the action.

    If the action is not retrievable, returns None.

    In general, there should be no surprises with this method. If you
    have established that the action is retrievable, (by asserting that
    [`action.retrievability_info`
    ][conatus.actions.action.Action.retrievability_info] is an instance of
    [`RetrievableActionInfo`
    ][conatus.actions.retrievability.RetrievableActionInfo]),
    then you should be sure that this method will the instructions.
    Nevertheless, if the JSON serialization of the changes fails,
    we will raise a `TypeError`.

    Returns:
        The retrievability instructions for the action, or None if the
        action is not retrievable.

    Raises:
        TypeError: If the changes are not JSON serializable.
    """
    if isinstance(self.retrievability_info, RetrievableActionInfo):
        try:
            changes_json = json.dumps(self._changes)
        except TypeError as e:
            msg = (
                "Even though the action is marked as retrievable, "
                "we were not able to serialize the changes. "
                "This is a bug, please report it."
            )
            raise TypeError(msg) from e
        return self.retrievability_info.get_retrievability_instructions(
            changes_as_json=changes_json if changes_json != "{}" else None,
        )
    return None

get_docstring

get_docstring(*, include_name: bool = True) -> str

Get the docstring of the action.

This docstring is meant to be used in the AI interfaces.

We convert it to Numpy style because we don't have to deal with the indentation of the parameters.

PARAMETER DESCRIPTION
include_name

Whether to include the name of the action in the docstring. Defaults to True.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
str

The docstring.

TYPE: str

Source code in conatus/actions/action.py
def get_docstring(self, *, include_name: bool = True) -> str:  # noqa: C901
    """Get the docstring of the action.

    This docstring is meant to be used in the AI interfaces.

    We convert it to Numpy style because we don't have to deal with
    the indentation of the parameters.

    Args:
        include_name (bool): Whether to include the name of the action in
            the docstring. Defaults to True.

    Returns:
        str: The docstring.
    """
    function_info = self.function_info
    lines: list[str] = []

    if include_name:
        lines.extend([f"Name: '{function_info.fn_name}'", ""])

    # Description section
    if function_info.description:
        lines.extend(
            ["Description", "-----------", function_info.description, ""]
        )

    # Parameters section
    if function_info.parameters:
        lines.extend(["Parameters", "----------"])
        for param_name, param_info in function_info.parameters.items():
            # Type hint
            lines.append(f"{param_name} : {param_info.type_hint_for_llm}")
            # Description
            if param_info.description:
                desc_lines = param_info.description.strip().split("\n")
                lines.extend(f"    {desc_line}" for desc_line in desc_lines)
            # Default value
            dv = param_info.default_value
            if dv is not None and dv is not Ellipsis:
                lines.append(f"    Default: {dv!r}")
            lines.append("")

    # Returns section
    if isinstance(function_info.returns, ReturnInfo):
        lines.extend(
            [
                "Returns",
                "-------",
                f"{function_info.returns.type_hint_for_llm}",
            ]
        )
        if function_info.returns.description:
            desc_lines = function_info.returns.description.strip().split(
                "\n"
            )
            lines.extend(f"    {line}" for line in desc_lines)
    else:  # list[ReturnInfo]
        lines.extend(["Returns", "-------"])
        for i, return_info in enumerate(function_info.returns):
            lines.append(f"{i} : {return_info.type_hint_for_llm}")
            if return_info.description:
                desc_lines = return_info.description.strip().split("\n")
                lines.extend(f"    {line}" for line in desc_lines)

    # Join everything together into a single docstring
    return "\n".join(lines).rstrip()  # strip trailing whitespace/newlines

add_runtime_state

add_runtime_state(runtime_state: RuntimeState) -> None

Add a pointer to the runtime state to the action.

This is useful if you want to use the runtime state in the action.

PARAMETER DESCRIPTION
runtime_state

The runtime state to add to the action.

TYPE: RuntimeState

Source code in conatus/actions/action.py
def add_runtime_state(self, runtime_state: RuntimeState) -> None:
    """Add a pointer to the runtime state to the action.

    This is useful if you want to use the runtime state in the action.

    Args:
        runtime_state: The runtime state to add to the action.
    """
    self.runtime_state = runtime_state

force_variable_update

force_variable_update(variable: ParamType) -> None

Force a variable update.

An Action, especially passive ones, can force a variable update.

This is useful, for instance, if you want to ensure that a browser screenshot is saved at a given step.

Note that we compare the variable object by checking its id. If the variable object is present multiple times in the runtime state, it will be updated in all of them.

PARAMETER DESCRIPTION
variable

The variable to update.

TYPE: ParamType

Source code in conatus/actions/action.py
def force_variable_update(self, variable: ParamType) -> None:
    """Force a variable update.

    An [`Action`][conatus.actions.action.Action], especially passive
    ones, can force a variable update.

    This is useful, for instance, if you want to ensure that a browser
    screenshot is saved at a given step.

    Note that we compare the variable object by checking its [`id`][id].
    If the variable object is present multiple times in the runtime state,
    it will be updated in all of them.

    Args:
        variable: The variable to update.
    """
    if self.runtime_state is None:
        return
    variable_id = id(variable)
    variable_exists = False
    for var in self.runtime_state.variables.values():
        if id(var.value) == variable_id:
            logger.debug(
                "Updating variable %s with value %s",
                var.name,
                variable,
            )
            variable_exists = True
            _ = var.update(
                value=variable,
                step=self.runtime_state.step_count,
                skip_if_equal=False,
            )
            self.runtime_state.pending_variable_updates.append(var.name)
    if not variable_exists:
        _ = self.runtime_state.add_result(result=variable)

check_params

check_params(
    *args: ParamType, **kwargs: ParamType
) -> dict[str, ParamType]

Check the parameters of the action.

This method is used to check the parameters of the action.

PARAMETER DESCRIPTION
*args

Positional arguments passed to the action function.

TYPE: ParamType DEFAULT: ()

**kwargs

Keyword arguments passed to the action function.

TYPE: ParamType DEFAULT: {}

RETURNS DESCRIPTION
dict[str, ParamType]

A dictionary with the parameters as keys and their values as values.

Source code in conatus/actions/action.py
def check_params(
    self, *args: ParamType, **kwargs: ParamType
) -> dict[str, ParamType]:
    """Check the parameters of the action.

    This method is used to check the parameters of the action.

    Args:
        *args: Positional arguments passed to the action function.
        **kwargs: Keyword arguments passed to the action function.

    Returns:
        A dictionary with the parameters as keys and their values as values.
    """
    return check_params(self.function_info, *args, **kwargs)

TypedAction

Bases: Action, Generic[Ps, R]

Base class for typed actions.

Warning

This class should not be instantiated directly. Instead, you should create a subclass of TypedAction and define an action function within it.

ATTRIBUTE DESCRIPTION
_action_function

The function that the action will call. See more in the Action class docstring. The difference with the equivalent Action attribute is that this one is typed.

TYPE: Callable[Ps, R]

retrievability_info instance-attribute

Information about the retrievability of the action.

If the action is retrievable: - this attribute is a RetrievableActionInfo instance. - the information enables you to retrieve the original action or function when recipes are executed. - you can use the get_retrievability_instructions method to get the instructions needed to retrieve the action in a new session.

If the action is not retrievable: - this attribute is a NonRetrievableActionInfo instance. - the information enables you to understand why the action is not retrievable.

This is set automatically by the ActionBlueprint metaclass.

always_available instance-attribute

always_available: bool

If True, the action is always available to the agent.

We determine this automatically by checking if all the parameters of the action are either JSON-serializable or optional.

Warning

Please do not set this attribute manually. If you do so, you risk breaking the agent's behavior.

discriminator_keys instance-attribute

discriminator_keys: list[str]

Keys of the discriminator parameters

If not empty, the action will be available only if the agent has access to variables compatible with the discriminator parameters.

override_type_hint_for_llm class-attribute instance-attribute

override_type_hint_for_llm: bool = False

If True, the type hints of the function will be ignored and replaced by a more LLM-friendly version.

desc class-attribute instance-attribute

desc: str = ''

A small, LLM-friendly description of the action.

passive class-attribute instance-attribute

passive: bool = False

Whether the action is passive.

If True, using the action during a run will not add it to the Recipe. In other words, it's an action that is useful for the LLM to understand the state of the world, but useless for the runtime to execute. One characteristic of passive actions is that they cannot return anything; the only thing they can do is (optionally) print to the screen.

hide_if_computer_use_mode class-attribute instance-attribute

hide_if_computer_use_mode: bool = False

Hide this action if the AI uses computer use mode.

This is meant so not as to confuse the AI by offering actions that conflict with the native capabilities of the computer use mode.

is_termination_action class-attribute instance-attribute

is_termination_action: bool = False

Flag to indicate if the action is a termination action.

This is used to indicate that the action is a termination action, i.e. an action that generally doesn't execute anything, but rather signals the end of the conversation.

runtime_state instance-attribute

runtime_state: RuntimeState | None

Pointer to the runtime state, if available.

force_update_on instance-attribute

force_update_on: list[str]

Names of the input variables that will be checked for changes.

function_info property

function_info: FunctionInfo

The function info of the action.

from conatus import action

@action
def double(x: int = 0) -> int:
    """Double the input.

    Args:
        x: The input to double.

    Returns:
        The double of the input.
    """
    return x * 2

print(double.function_info.pretty_print())
# Function info:
# - Name: 'double'
# - Description: 'Double the input.'
# - Parameter x:
#     - Description: The input to double.
#     - Type hint: <class 'int'> (JSON OK)
#     -- JSON-serializable subtype: <class 'int'>
#     -- Type of type hint: <class 'type'>
#     - Type hint shown to the LLM: 'int'
#     - Required: True
#     - Kind of parameter: POSITIONAL_OR_KEYWORD (1)
#     - Default value: 0
# - Returns:
#     - Description: The double of the input.
#     - Type hint: <class 'int'>
#     -- Type of type hint: <class 'type'>
#     - Type hint shown to the LLM: 'int'

pydantic_models property

pydantic_models: FunctionPydanticModels

The pydantic models of the action.

The object returned is a FunctionPydanticModels instance. From that, you can retrieve the following attributes:

  • input_model : The Pydantic model for the parameters of the action.
  • output_model : The Pydantic model for the output of the action.
  • json_schema : The Pydantic model for the JSON schema of the action.
  • all_fields : A dictionary containing all the fields of the action, with their name as the key and a tuple of the type and the FieldInfo instance as the value. This is mostly for internal use.

name property writable

name: str

The name of the action function.

It is automatically set at class creation time: - If the class is created through the @action decorator, the name will be the name of the function that was decorated. - If the class is created through the Action class, the name will be the snake case version of the class name.

It can also be set manually, as long as it is not in the list of reserved action names.

openai_schema property

openai_schema: FunctionDefinition

The OpenAI schema of the action function.

json_schema property

json_schema: type[BaseModel]

The Pydantic model of the JSON schema of the action.

To print it more fully, you can use Pydantic's model_json_schema method:

from conatus import action

@action
def my_action(x: int) -> int:
    return x

print(my_action.json_schema.model_json_schema())

input_model property

input_model: type[BaseModel]

The Pydantic model to validate the input arguments of the action.

__new__

__new__(
    direct_execute: Literal[False],
    *args: None,
    **kwargs: None
) -> Self
__new__(*args: args, **kwargs: kwargs) -> R
__new__(*args: ParamType, **kwargs: ParamType) -> R | Self

Create a new TypedAction instance.

This method is called when a new instance of a TypedAction class is created. It will call the action function with the arguments passed and return the result, or the instance itself if direct_execute is set to False.

PARAMETER DESCRIPTION
*args

Positional arguments passed to the action function.

TYPE: ParamType DEFAULT: ()

**kwargs: Keyword arguments passed to the action function.

PARAMETER DESCRIPTION
direct_execute

If True, the action function will be called with the arguments passed to the constructor. If False, the instance will be returned.

TYPE: bool

RETURNS DESCRIPTION
R | Self

The result of the action function if direct_execute is True, or

R | Self

the instance itself otherwise.

Source code in conatus/actions/action.py
def __new__(  # type: ignore[misc] # noqa: D417
    cls,
    *args: ParamType,
    **kwargs: ParamType,
) -> R | Self:
    """Create a new TypedAction instance.

    This method is called when a new instance of a `TypedAction` class is
    created. It will call the action function with the arguments passed and
    return the result, or the instance itself if `direct_execute` is set to
    False.

    Args:
        *args: Positional arguments passed to the action function.
       **kwargs: Keyword arguments passed to the action function.

    Other Parameters:
        direct_execute (bool): If [`True`][True], the action function will
            be called with the arguments passed to the constructor. If
            [`False`][False], the instance will be returned.

    Returns:
        The result of the action function if `direct_execute` is True, or
        the instance itself otherwise.
    """
    instance = super(Action, cls).__new__(cls)
    instance.__init__()  # type: ignore[misc]
    if kwargs.pop("direct_execute", True):
        return instance._execute(
            *args,  # type: ignore[arg-type]
            **kwargs,  # type: ignore[arg-type]
        )  # pyright: ignore[reportCallIssue]
    return instance

__call__

__call__(*args: args, **kwargs: kwargs) -> R

Call the action function.

This is a convenience method that allows you to call the action function directly on the instance of the TypedAction class.

PARAMETER DESCRIPTION
*args

Positional arguments passed to the action function.

TYPE: args DEFAULT: ()

**kwargs

Keyword arguments passed to the action function.

TYPE: kwargs DEFAULT: {}

RETURNS DESCRIPTION
R

The result of the action function.

Source code in conatus/actions/action.py
@override
def __call__(self, *args: Ps.args, **kwargs: Ps.kwargs) -> R:  # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
    """Call the action function.

    This is a convenience method that allows you to call the action
    function directly on the instance of the `TypedAction` class.

    Args:
        *args: Positional arguments passed to the action function.
        **kwargs: Keyword arguments passed to the action function.

    Returns:
        The result of the action function.
    """
    return self._execute(*args, **kwargs)

execute

execute(*args: args, **kwargs: kwargs) -> R

Explicit execution of the action function.

This is useful if you've created an instance of an Action class with direct_execute=False and you want to execute the action function afterwards. This is also a way to make type checkers happy.

from conatus.actions.action import TypedAction, action_function
from typing import Concatenate

class MyTypedAction(TypedAction[Concatenate[int, int], int]):

    @Action.function
    def my_action(self, a: int, b: int) -> int:
        return a + b

assert MyTypedAction(1, 2) == 3  # This will work like a function...

# ... unless you want to separate it in two steps
instance = MyTypedAction(direct_execute=False)
assert type(instance) == MyTypedAction
assert instance.execute(1, 2) == 3
PARAMETER DESCRIPTION
*args

Positional arguments passed to the action function.

TYPE: args DEFAULT: ()

**kwargs

Keyword arguments passed to the action function.

TYPE: kwargs DEFAULT: {}

RETURNS DESCRIPTION
R

The result of the action function.

Source code in conatus/actions/action.py
@override
def execute(self, *args: Ps.args, **kwargs: Ps.kwargs) -> R:  # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
    """Explicit execution of the action function.

    This is useful if you've created an instance of an `Action` class with
    `direct_execute=False` and you want to execute the action function
    afterwards. This is also a way to make type checkers happy.

    ```python
    from conatus.actions.action import TypedAction, action_function
    from typing import Concatenate

    class MyTypedAction(TypedAction[Concatenate[int, int], int]):

        @Action.function
        def my_action(self, a: int, b: int) -> int:
            return a + b

    assert MyTypedAction(1, 2) == 3  # This will work like a function...

    # ... unless you want to separate it in two steps
    instance = MyTypedAction(direct_execute=False)
    assert type(instance) == MyTypedAction
    assert instance.execute(1, 2) == 3
    ```


    Args:
        *args: Positional arguments passed to the action function.
        **kwargs: Keyword arguments passed to the action function.

    Returns:
        The result of the action function.
    """
    return self._execute(*args, **kwargs)

function classmethod

function() -> Callable[[F], F]
function(func: None) -> Callable[[F], F]
function(func: F) -> F
function(func: F | None = None) -> F | Callable[[F], F]

Decorator to specify an action function within an Action subclass.

This method is not meant to be called directly. This only adds a bit of metadata to the function so that the class can later find it and properly execute it.

from conatus.actions.action import Action

class MyAction(Action):

    attr0 = 999

    def useful_method(self, a: int) -> int:
        return a + self.attr0

    # Now we know this is the action function
    @Action.function
    def my_action(self, a: int, b: int) -> int:
        return self.useful_method(a) + b

MyAction(1, 2)  # This will work and return 1002

Sync functions only

This decorator is meant to be used only with sync functions. We'll support async functions in the future.

One of the effects of this decorator is that it will essentially ensure that the self argument is removed from the schema we build out of the signature and docstring of the function. This is fine 99.9% of the times, but if you have defined a static method whose first argument is somehow named self, you are going to get unexpected behavior.

PARAMETER DESCRIPTION
func

The function that the action will call.

TYPE: F | None DEFAULT: None

RETURNS DESCRIPTION
F | Callable[[F], F]

The same function that was passed as an argument.

Source code in conatus/actions/action.py
@classmethod
def function(cls, func: F | None = None) -> F | Callable[[F], F]:
    """Decorator to specify an action function within an `Action` subclass.

    This method is not meant to be called directly. This only adds a bit of
    metadata to the function so that the class can later find it and
    properly execute it.

    ```python
    from conatus.actions.action import Action

    class MyAction(Action):

        attr0 = 999

        def useful_method(self, a: int) -> int:
            return a + self.attr0

        # Now we know this is the action function
        @Action.function
        def my_action(self, a: int, b: int) -> int:
            return self.useful_method(a) + b

    MyAction(1, 2)  # This will work and return 1002
    ```

    !!! note "Sync functions only"

        This decorator is meant to be used only with sync functions. We'll
        support async functions in the future.

    One of the effects of this decorator is that it will essentially ensure
    that the self argument is removed from the schema we build out of the
    signature and docstring of the function. This is fine 99.9% of the
    times, but if you have defined a static method whose first argument is
    somehow named self, you are going to get unexpected behavior.

    Args:
        func: The function that the action will call.

    Returns:
        The same function that was passed as an argument.
    """
    # NOTE: If we end up allowing users to pass arguments to this decorator,
    # we'll have to use a wrapper function here.
    if func is None:
        return mark_action_function
    return mark_action_function(func)

is_action classmethod

is_action(
    action: Self | ActionBlueprint | FunctionType,
) -> Literal["class", "instance", "none"]

Check if the action is an instance or class of Action.

RETURNS DESCRIPTION
Literal['class', 'instance', 'none']

"class" if the action is a class of Action, "instance" if it is an instance of Action, and "none" if it is neither.

Source code in conatus/actions/action.py
@classmethod
def is_action(
    cls, action: Self | ActionBlueprint | FunctionType
) -> Literal["class", "instance", "none"]:
    """Check if the action is an instance or class of `Action`.

    Returns:
        "class" if the action is a class of `Action`, "instance" if it is
            an instance of `Action`, and "none" if it is neither.
    """
    if isinstance(action, Action):
        return "instance"
    if isinstance(action, type) and issubclass(action, Action):
        return "class"
    return "none"

__deepcopy__

__deepcopy__(memo: dict[int, Action]) -> Self

Deepcopy the action.

This method is called when a deep copy of the action is made.

PARAMETER DESCRIPTION
memo

The memo dictionary.

TYPE: dict[int, Action]

RETURNS DESCRIPTION
Self

The deep copy of the action.

Source code in conatus/actions/action.py
def __deepcopy__(self: Self, memo: dict[int, Action]) -> Self:
    """Deepcopy the action.

    This method is called when a deep copy of the action is made.

    Args:
        memo: The memo dictionary.

    Returns:
        The deep copy of the action.
    """
    cls = self.__class__  # Extract the class of the object
    result = cls.__new__(cls, direct_execute=False)
    memo[id(self)] = result
    for k, v in self.__dict__.items():  # pyright: ignore[reportAny]
        setattr(
            result,
            k,
            copy.deepcopy(v, memo),  # pyright: ignore[reportAny]
        )
    return result

__setattr__

__setattr__(name: str, value: ParamType) -> None

Set an attribute on the instance.

We use this function to intercept attribute changes.

PARAMETER DESCRIPTION
name

The name of the attribute.

TYPE: str

value

The value of the attribute.

TYPE: ParamType

Source code in conatus/actions/action.py
@override
def __setattr__(self, name: str, value: ParamType) -> None:
    """Set an attribute on the instance.

    We use this function to intercept attribute changes.

    Args:
        name: The name of the attribute.
        value: The value of the attribute.
    """
    skip_intercept_for = [
        "_intercept_setattr",
        "_changes",
        "retrievability_info",
        "__orig_class__",
        "_action_function",
        "_function_info",
        "_pydantic_models",
        "runtime_state",
    ]
    if self._intercept_setattr and name not in skip_intercept_for:
        if not is_json_serializable_value(value) and isinstance(
            self.retrievability_info, RetrievableActionInfo
        ):
            self.retrievability_info = (
                self.retrievability_info.change_to_non_retrievable(
                    "changes_not_json_serializable"
                )
            )
        self._changes[name] = value
    super().__setattr__(name, value)

get_openai_schema

get_openai_schema(
    *, strict_mode: bool
) -> FunctionDefinition

The OpenAI schema of the action function.

This method is meant to be used if you want to explicitly pass the strict_mode argument to the get_openai_schema method. Otherwise, you should use the openai_schema property. This is why strict_mode is a required argument.

PARAMETER DESCRIPTION
strict_mode

If True, the schema will be generated in strict mode. If False, the schema will be generated in non-strict mode.

TYPE: bool

RETURNS DESCRIPTION
FunctionDefinition

The OpenAI schema of the action function.

Source code in conatus/actions/action.py
def get_openai_schema(self, *, strict_mode: bool) -> FunctionDefinition:
    """The OpenAI schema of the action function.

    This method is meant to be used if you want to explicitly pass the
    `strict_mode` argument to the `get_openai_schema` method. Otherwise,
    you should use the `openai_schema` property. This is why `strict_mode`
    is a required argument.

    Args:
        strict_mode: If True, the schema will be generated in strict mode.
            If False, the schema will be generated in non-strict mode.

    Returns:
        The OpenAI schema of the action function.
    """
    return generate_openai_json_schema(
        self.json_schema,
        strict_mode=strict_mode,
    )

pretty_function_info

pretty_function_info() -> None

Pretty-print the function information.

Source code in conatus/actions/action.py
def pretty_function_info(self) -> None:  # pragma: no cover
    """Pretty-print the function information."""
    print(self._function_info.pretty_print(colors=True))  # noqa: T201

get_maybe_recipe_retrievability_warnings

get_maybe_recipe_retrievability_warnings(
    i: int, *, with_first_message: bool = True
) -> str | None

Generate a warning message if the action is not retrievable.

Useful when are creating an Agent and want to display the warnings then.

PARAMETER DESCRIPTION
i

The index of the action in the list of actions

TYPE: int

with_first_message

If True, the first message will be included. Defaults to True.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
str | None

The warning message if the action is not retrievable, or None

str | None

otherwise.

Source code in conatus/actions/action.py
def get_maybe_recipe_retrievability_warnings(
    self,
    i: int,
    *,
    with_first_message: bool = True,
) -> str | None:
    """Generate a warning message if the action is not retrievable.

    Useful when are creating an [`Agent`][conatus.agents.Agent]
    and want to display the warnings then.

    Args:
        i: The index of the action in the list of actions
        with_first_message: If True, the first message will be included.
            Defaults to True.

    Returns:
        The warning message if the action is not retrievable, or None
        otherwise.
    """
    msg = (
        (
            "\n⚠️IMPORTANT: You can run the agent, but you will NOT be"
            " able to write a recipe with it.\n"
            "This is because you have at least one action that cannot"
            " be retrieved:\n\n"
        )
        if with_first_message
        else ""
    )
    if isinstance(self.retrievability_info, NonRetrievableActionInfo):
        msg += f"- {self.retrievability_info.origin_qualname} "
        msg += f"({self._to_ith(i)} in your list)\n"
        match self.retrievability_info.reason_why_not_retrievable:
            case "within_locals":
                msg += (
                    "  It is within the locals of a function. \n"
                    "  You will not be able to write a recipe with it "
                    "unless you move it outside of that function."
                )
            case "within_main":  # pragma: no cover
                msg += (
                    "  It is within the main block of a script. \n"
                    "  You will not be able to write a recipe with it "
                    "unless you move it to a file."
                )
            case "changes_not_json_serializable":  # pragma: no branch
                msg += (
                    "  Some of its attributes have changed, and "
                    "these new attributes are not JSON serializable.\n"
                    "  You will not be "
                    "able to write a recipe with it unless you: \n"
                    "    1. Do not perform any changes to the action's "
                    "attributes before passing it to the agent, \n"
                    "    2. If you do need to perform changes,"
                    " make sure "
                    "that the new values are JSON serializable."
                )
        return msg
    return None

get_maybe_recipe_retrievability_instructions

get_maybe_recipe_retrievability_instructions() -> (
    RetrievabilityInstructions | None
)

Get the retrievability instructions for the action.

If the action is not retrievable, returns None.

In general, there should be no surprises with this method. If you have established that the action is retrievable, (by asserting that action.retrievability_info is an instance of RetrievableActionInfo ), then you should be sure that this method will the instructions. Nevertheless, if the JSON serialization of the changes fails, we will raise a TypeError.

RETURNS DESCRIPTION
RetrievabilityInstructions | None

The retrievability instructions for the action, or None if the

RetrievabilityInstructions | None

action is not retrievable.

RAISES DESCRIPTION
TypeError

If the changes are not JSON serializable.

Source code in conatus/actions/action.py
def get_maybe_recipe_retrievability_instructions(
    self,
) -> RetrievabilityInstructions | None:
    """Get the retrievability instructions for the action.

    If the action is not retrievable, returns None.

    In general, there should be no surprises with this method. If you
    have established that the action is retrievable, (by asserting that
    [`action.retrievability_info`
    ][conatus.actions.action.Action.retrievability_info] is an instance of
    [`RetrievableActionInfo`
    ][conatus.actions.retrievability.RetrievableActionInfo]),
    then you should be sure that this method will the instructions.
    Nevertheless, if the JSON serialization of the changes fails,
    we will raise a `TypeError`.

    Returns:
        The retrievability instructions for the action, or None if the
        action is not retrievable.

    Raises:
        TypeError: If the changes are not JSON serializable.
    """
    if isinstance(self.retrievability_info, RetrievableActionInfo):
        try:
            changes_json = json.dumps(self._changes)
        except TypeError as e:
            msg = (
                "Even though the action is marked as retrievable, "
                "we were not able to serialize the changes. "
                "This is a bug, please report it."
            )
            raise TypeError(msg) from e
        return self.retrievability_info.get_retrievability_instructions(
            changes_as_json=changes_json if changes_json != "{}" else None,
        )
    return None

get_docstring

get_docstring(*, include_name: bool = True) -> str

Get the docstring of the action.

This docstring is meant to be used in the AI interfaces.

We convert it to Numpy style because we don't have to deal with the indentation of the parameters.

PARAMETER DESCRIPTION
include_name

Whether to include the name of the action in the docstring. Defaults to True.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
str

The docstring.

TYPE: str

Source code in conatus/actions/action.py
def get_docstring(self, *, include_name: bool = True) -> str:  # noqa: C901
    """Get the docstring of the action.

    This docstring is meant to be used in the AI interfaces.

    We convert it to Numpy style because we don't have to deal with
    the indentation of the parameters.

    Args:
        include_name (bool): Whether to include the name of the action in
            the docstring. Defaults to True.

    Returns:
        str: The docstring.
    """
    function_info = self.function_info
    lines: list[str] = []

    if include_name:
        lines.extend([f"Name: '{function_info.fn_name}'", ""])

    # Description section
    if function_info.description:
        lines.extend(
            ["Description", "-----------", function_info.description, ""]
        )

    # Parameters section
    if function_info.parameters:
        lines.extend(["Parameters", "----------"])
        for param_name, param_info in function_info.parameters.items():
            # Type hint
            lines.append(f"{param_name} : {param_info.type_hint_for_llm}")
            # Description
            if param_info.description:
                desc_lines = param_info.description.strip().split("\n")
                lines.extend(f"    {desc_line}" for desc_line in desc_lines)
            # Default value
            dv = param_info.default_value
            if dv is not None and dv is not Ellipsis:
                lines.append(f"    Default: {dv!r}")
            lines.append("")

    # Returns section
    if isinstance(function_info.returns, ReturnInfo):
        lines.extend(
            [
                "Returns",
                "-------",
                f"{function_info.returns.type_hint_for_llm}",
            ]
        )
        if function_info.returns.description:
            desc_lines = function_info.returns.description.strip().split(
                "\n"
            )
            lines.extend(f"    {line}" for line in desc_lines)
    else:  # list[ReturnInfo]
        lines.extend(["Returns", "-------"])
        for i, return_info in enumerate(function_info.returns):
            lines.append(f"{i} : {return_info.type_hint_for_llm}")
            if return_info.description:
                desc_lines = return_info.description.strip().split("\n")
                lines.extend(f"    {line}" for line in desc_lines)

    # Join everything together into a single docstring
    return "\n".join(lines).rstrip()  # strip trailing whitespace/newlines

add_runtime_state

add_runtime_state(runtime_state: RuntimeState) -> None

Add a pointer to the runtime state to the action.

This is useful if you want to use the runtime state in the action.

PARAMETER DESCRIPTION
runtime_state

The runtime state to add to the action.

TYPE: RuntimeState

Source code in conatus/actions/action.py
def add_runtime_state(self, runtime_state: RuntimeState) -> None:
    """Add a pointer to the runtime state to the action.

    This is useful if you want to use the runtime state in the action.

    Args:
        runtime_state: The runtime state to add to the action.
    """
    self.runtime_state = runtime_state

force_variable_update

force_variable_update(variable: ParamType) -> None

Force a variable update.

An Action, especially passive ones, can force a variable update.

This is useful, for instance, if you want to ensure that a browser screenshot is saved at a given step.

Note that we compare the variable object by checking its id. If the variable object is present multiple times in the runtime state, it will be updated in all of them.

PARAMETER DESCRIPTION
variable

The variable to update.

TYPE: ParamType

Source code in conatus/actions/action.py
def force_variable_update(self, variable: ParamType) -> None:
    """Force a variable update.

    An [`Action`][conatus.actions.action.Action], especially passive
    ones, can force a variable update.

    This is useful, for instance, if you want to ensure that a browser
    screenshot is saved at a given step.

    Note that we compare the variable object by checking its [`id`][id].
    If the variable object is present multiple times in the runtime state,
    it will be updated in all of them.

    Args:
        variable: The variable to update.
    """
    if self.runtime_state is None:
        return
    variable_id = id(variable)
    variable_exists = False
    for var in self.runtime_state.variables.values():
        if id(var.value) == variable_id:
            logger.debug(
                "Updating variable %s with value %s",
                var.name,
                variable,
            )
            variable_exists = True
            _ = var.update(
                value=variable,
                step=self.runtime_state.step_count,
                skip_if_equal=False,
            )
            self.runtime_state.pending_variable_updates.append(var.name)
    if not variable_exists:
        _ = self.runtime_state.add_result(result=variable)

check_params

check_params(
    *args: ParamType, **kwargs: ParamType
) -> dict[str, ParamType]

Check the parameters of the action.

This method is used to check the parameters of the action.

PARAMETER DESCRIPTION
*args

Positional arguments passed to the action function.

TYPE: ParamType DEFAULT: ()

**kwargs

Keyword arguments passed to the action function.

TYPE: ParamType DEFAULT: {}

RETURNS DESCRIPTION
dict[str, ParamType]

A dictionary with the parameters as keys and their values as values.

Source code in conatus/actions/action.py
def check_params(
    self, *args: ParamType, **kwargs: ParamType
) -> dict[str, ParamType]:
    """Check the parameters of the action.

    This method is used to check the parameters of the action.

    Args:
        *args: Positional arguments passed to the action function.
        **kwargs: Keyword arguments passed to the action function.

    Returns:
        A dictionary with the parameters as keys and their values as values.
    """
    return check_params(self.function_info, *args, **kwargs)

UntypedAction

Bases: TypedAction[P, ParamType]

Untyped action.

retrievability_info instance-attribute

Information about the retrievability of the action.

If the action is retrievable: - this attribute is a RetrievableActionInfo instance. - the information enables you to retrieve the original action or function when recipes are executed. - you can use the get_retrievability_instructions method to get the instructions needed to retrieve the action in a new session.

If the action is not retrievable: - this attribute is a NonRetrievableActionInfo instance. - the information enables you to understand why the action is not retrievable.

This is set automatically by the ActionBlueprint metaclass.

always_available instance-attribute

always_available: bool

If True, the action is always available to the agent.

We determine this automatically by checking if all the parameters of the action are either JSON-serializable or optional.

Warning

Please do not set this attribute manually. If you do so, you risk breaking the agent's behavior.

discriminator_keys instance-attribute

discriminator_keys: list[str]

Keys of the discriminator parameters

If not empty, the action will be available only if the agent has access to variables compatible with the discriminator parameters.

override_type_hint_for_llm class-attribute instance-attribute

override_type_hint_for_llm: bool = False

If True, the type hints of the function will be ignored and replaced by a more LLM-friendly version.

desc class-attribute instance-attribute

desc: str = ''

A small, LLM-friendly description of the action.

passive class-attribute instance-attribute

passive: bool = False

Whether the action is passive.

If True, using the action during a run will not add it to the Recipe. In other words, it's an action that is useful for the LLM to understand the state of the world, but useless for the runtime to execute. One characteristic of passive actions is that they cannot return anything; the only thing they can do is (optionally) print to the screen.

hide_if_computer_use_mode class-attribute instance-attribute

hide_if_computer_use_mode: bool = False

Hide this action if the AI uses computer use mode.

This is meant so not as to confuse the AI by offering actions that conflict with the native capabilities of the computer use mode.

is_termination_action class-attribute instance-attribute

is_termination_action: bool = False

Flag to indicate if the action is a termination action.

This is used to indicate that the action is a termination action, i.e. an action that generally doesn't execute anything, but rather signals the end of the conversation.

runtime_state instance-attribute

runtime_state: RuntimeState | None

Pointer to the runtime state, if available.

force_update_on instance-attribute

force_update_on: list[str]

Names of the input variables that will be checked for changes.

function_info property

function_info: FunctionInfo

The function info of the action.

from conatus import action

@action
def double(x: int = 0) -> int:
    """Double the input.

    Args:
        x: The input to double.

    Returns:
        The double of the input.
    """
    return x * 2

print(double.function_info.pretty_print())
# Function info:
# - Name: 'double'
# - Description: 'Double the input.'
# - Parameter x:
#     - Description: The input to double.
#     - Type hint: <class 'int'> (JSON OK)
#     -- JSON-serializable subtype: <class 'int'>
#     -- Type of type hint: <class 'type'>
#     - Type hint shown to the LLM: 'int'
#     - Required: True
#     - Kind of parameter: POSITIONAL_OR_KEYWORD (1)
#     - Default value: 0
# - Returns:
#     - Description: The double of the input.
#     - Type hint: <class 'int'>
#     -- Type of type hint: <class 'type'>
#     - Type hint shown to the LLM: 'int'

pydantic_models property

pydantic_models: FunctionPydanticModels

The pydantic models of the action.

The object returned is a FunctionPydanticModels instance. From that, you can retrieve the following attributes:

  • input_model : The Pydantic model for the parameters of the action.
  • output_model : The Pydantic model for the output of the action.
  • json_schema : The Pydantic model for the JSON schema of the action.
  • all_fields : A dictionary containing all the fields of the action, with their name as the key and a tuple of the type and the FieldInfo instance as the value. This is mostly for internal use.

name property writable

name: str

The name of the action function.

It is automatically set at class creation time: - If the class is created through the @action decorator, the name will be the name of the function that was decorated. - If the class is created through the Action class, the name will be the snake case version of the class name.

It can also be set manually, as long as it is not in the list of reserved action names.

openai_schema property

openai_schema: FunctionDefinition

The OpenAI schema of the action function.

json_schema property

json_schema: type[BaseModel]

The Pydantic model of the JSON schema of the action.

To print it more fully, you can use Pydantic's model_json_schema method:

from conatus import action

@action
def my_action(x: int) -> int:
    return x

print(my_action.json_schema.model_json_schema())

input_model property

input_model: type[BaseModel]

The Pydantic model to validate the input arguments of the action.

function classmethod

function() -> Callable[[F], F]
function(func: None) -> Callable[[F], F]
function(func: F) -> F
function(func: F | None = None) -> F | Callable[[F], F]

Decorator to specify an action function within an Action subclass.

This method is not meant to be called directly. This only adds a bit of metadata to the function so that the class can later find it and properly execute it.

from conatus.actions.action import Action

class MyAction(Action):

    attr0 = 999

    def useful_method(self, a: int) -> int:
        return a + self.attr0

    # Now we know this is the action function
    @Action.function
    def my_action(self, a: int, b: int) -> int:
        return self.useful_method(a) + b

MyAction(1, 2)  # This will work and return 1002

Sync functions only

This decorator is meant to be used only with sync functions. We'll support async functions in the future.

One of the effects of this decorator is that it will essentially ensure that the self argument is removed from the schema we build out of the signature and docstring of the function. This is fine 99.9% of the times, but if you have defined a static method whose first argument is somehow named self, you are going to get unexpected behavior.

PARAMETER DESCRIPTION
func

The function that the action will call.

TYPE: F | None DEFAULT: None

RETURNS DESCRIPTION
F | Callable[[F], F]

The same function that was passed as an argument.

Source code in conatus/actions/action.py
@classmethod
def function(cls, func: F | None = None) -> F | Callable[[F], F]:
    """Decorator to specify an action function within an `Action` subclass.

    This method is not meant to be called directly. This only adds a bit of
    metadata to the function so that the class can later find it and
    properly execute it.

    ```python
    from conatus.actions.action import Action

    class MyAction(Action):

        attr0 = 999

        def useful_method(self, a: int) -> int:
            return a + self.attr0

        # Now we know this is the action function
        @Action.function
        def my_action(self, a: int, b: int) -> int:
            return self.useful_method(a) + b

    MyAction(1, 2)  # This will work and return 1002
    ```

    !!! note "Sync functions only"

        This decorator is meant to be used only with sync functions. We'll
        support async functions in the future.

    One of the effects of this decorator is that it will essentially ensure
    that the self argument is removed from the schema we build out of the
    signature and docstring of the function. This is fine 99.9% of the
    times, but if you have defined a static method whose first argument is
    somehow named self, you are going to get unexpected behavior.

    Args:
        func: The function that the action will call.

    Returns:
        The same function that was passed as an argument.
    """
    # NOTE: If we end up allowing users to pass arguments to this decorator,
    # we'll have to use a wrapper function here.
    if func is None:
        return mark_action_function
    return mark_action_function(func)

is_action classmethod

is_action(
    action: Self | ActionBlueprint | FunctionType,
) -> Literal["class", "instance", "none"]

Check if the action is an instance or class of Action.

RETURNS DESCRIPTION
Literal['class', 'instance', 'none']

"class" if the action is a class of Action, "instance" if it is an instance of Action, and "none" if it is neither.

Source code in conatus/actions/action.py
@classmethod
def is_action(
    cls, action: Self | ActionBlueprint | FunctionType
) -> Literal["class", "instance", "none"]:
    """Check if the action is an instance or class of `Action`.

    Returns:
        "class" if the action is a class of `Action`, "instance" if it is
            an instance of `Action`, and "none" if it is neither.
    """
    if isinstance(action, Action):
        return "instance"
    if isinstance(action, type) and issubclass(action, Action):
        return "class"
    return "none"

__deepcopy__

__deepcopy__(memo: dict[int, Action]) -> Self

Deepcopy the action.

This method is called when a deep copy of the action is made.

PARAMETER DESCRIPTION
memo

The memo dictionary.

TYPE: dict[int, Action]

RETURNS DESCRIPTION
Self

The deep copy of the action.

Source code in conatus/actions/action.py
def __deepcopy__(self: Self, memo: dict[int, Action]) -> Self:
    """Deepcopy the action.

    This method is called when a deep copy of the action is made.

    Args:
        memo: The memo dictionary.

    Returns:
        The deep copy of the action.
    """
    cls = self.__class__  # Extract the class of the object
    result = cls.__new__(cls, direct_execute=False)
    memo[id(self)] = result
    for k, v in self.__dict__.items():  # pyright: ignore[reportAny]
        setattr(
            result,
            k,
            copy.deepcopy(v, memo),  # pyright: ignore[reportAny]
        )
    return result

__new__

__new__(
    direct_execute: Literal[False],
    *args: None,
    **kwargs: None
) -> Self
__new__(*args: args, **kwargs: kwargs) -> R
__new__(*args: ParamType, **kwargs: ParamType) -> R | Self

Create a new TypedAction instance.

This method is called when a new instance of a TypedAction class is created. It will call the action function with the arguments passed and return the result, or the instance itself if direct_execute is set to False.

PARAMETER DESCRIPTION
*args

Positional arguments passed to the action function.

TYPE: ParamType DEFAULT: ()

**kwargs: Keyword arguments passed to the action function.

PARAMETER DESCRIPTION
direct_execute

If True, the action function will be called with the arguments passed to the constructor. If False, the instance will be returned.

TYPE: bool

RETURNS DESCRIPTION
R | Self

The result of the action function if direct_execute is True, or

R | Self

the instance itself otherwise.

Source code in conatus/actions/action.py
def __new__(  # type: ignore[misc] # noqa: D417
    cls,
    *args: ParamType,
    **kwargs: ParamType,
) -> R | Self:
    """Create a new TypedAction instance.

    This method is called when a new instance of a `TypedAction` class is
    created. It will call the action function with the arguments passed and
    return the result, or the instance itself if `direct_execute` is set to
    False.

    Args:
        *args: Positional arguments passed to the action function.
       **kwargs: Keyword arguments passed to the action function.

    Other Parameters:
        direct_execute (bool): If [`True`][True], the action function will
            be called with the arguments passed to the constructor. If
            [`False`][False], the instance will be returned.

    Returns:
        The result of the action function if `direct_execute` is True, or
        the instance itself otherwise.
    """
    instance = super(Action, cls).__new__(cls)
    instance.__init__()  # type: ignore[misc]
    if kwargs.pop("direct_execute", True):
        return instance._execute(
            *args,  # type: ignore[arg-type]
            **kwargs,  # type: ignore[arg-type]
        )  # pyright: ignore[reportCallIssue]
    return instance

__call__

__call__(*args: args, **kwargs: kwargs) -> R

Call the action function.

This is a convenience method that allows you to call the action function directly on the instance of the TypedAction class.

PARAMETER DESCRIPTION
*args

Positional arguments passed to the action function.

TYPE: args DEFAULT: ()

**kwargs

Keyword arguments passed to the action function.

TYPE: kwargs DEFAULT: {}

RETURNS DESCRIPTION
R

The result of the action function.

Source code in conatus/actions/action.py
@override
def __call__(self, *args: Ps.args, **kwargs: Ps.kwargs) -> R:  # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
    """Call the action function.

    This is a convenience method that allows you to call the action
    function directly on the instance of the `TypedAction` class.

    Args:
        *args: Positional arguments passed to the action function.
        **kwargs: Keyword arguments passed to the action function.

    Returns:
        The result of the action function.
    """
    return self._execute(*args, **kwargs)

__setattr__

__setattr__(name: str, value: ParamType) -> None

Set an attribute on the instance.

We use this function to intercept attribute changes.

PARAMETER DESCRIPTION
name

The name of the attribute.

TYPE: str

value

The value of the attribute.

TYPE: ParamType

Source code in conatus/actions/action.py
@override
def __setattr__(self, name: str, value: ParamType) -> None:
    """Set an attribute on the instance.

    We use this function to intercept attribute changes.

    Args:
        name: The name of the attribute.
        value: The value of the attribute.
    """
    skip_intercept_for = [
        "_intercept_setattr",
        "_changes",
        "retrievability_info",
        "__orig_class__",
        "_action_function",
        "_function_info",
        "_pydantic_models",
        "runtime_state",
    ]
    if self._intercept_setattr and name not in skip_intercept_for:
        if not is_json_serializable_value(value) and isinstance(
            self.retrievability_info, RetrievableActionInfo
        ):
            self.retrievability_info = (
                self.retrievability_info.change_to_non_retrievable(
                    "changes_not_json_serializable"
                )
            )
        self._changes[name] = value
    super().__setattr__(name, value)

execute

execute(*args: args, **kwargs: kwargs) -> R

Explicit execution of the action function.

This is useful if you've created an instance of an Action class with direct_execute=False and you want to execute the action function afterwards. This is also a way to make type checkers happy.

from conatus.actions.action import TypedAction, action_function
from typing import Concatenate

class MyTypedAction(TypedAction[Concatenate[int, int], int]):

    @Action.function
    def my_action(self, a: int, b: int) -> int:
        return a + b

assert MyTypedAction(1, 2) == 3  # This will work like a function...

# ... unless you want to separate it in two steps
instance = MyTypedAction(direct_execute=False)
assert type(instance) == MyTypedAction
assert instance.execute(1, 2) == 3
PARAMETER DESCRIPTION
*args

Positional arguments passed to the action function.

TYPE: args DEFAULT: ()

**kwargs

Keyword arguments passed to the action function.

TYPE: kwargs DEFAULT: {}

RETURNS DESCRIPTION
R

The result of the action function.

Source code in conatus/actions/action.py
@override
def execute(self, *args: Ps.args, **kwargs: Ps.kwargs) -> R:  # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
    """Explicit execution of the action function.

    This is useful if you've created an instance of an `Action` class with
    `direct_execute=False` and you want to execute the action function
    afterwards. This is also a way to make type checkers happy.

    ```python
    from conatus.actions.action import TypedAction, action_function
    from typing import Concatenate

    class MyTypedAction(TypedAction[Concatenate[int, int], int]):

        @Action.function
        def my_action(self, a: int, b: int) -> int:
            return a + b

    assert MyTypedAction(1, 2) == 3  # This will work like a function...

    # ... unless you want to separate it in two steps
    instance = MyTypedAction(direct_execute=False)
    assert type(instance) == MyTypedAction
    assert instance.execute(1, 2) == 3
    ```


    Args:
        *args: Positional arguments passed to the action function.
        **kwargs: Keyword arguments passed to the action function.

    Returns:
        The result of the action function.
    """
    return self._execute(*args, **kwargs)

get_openai_schema

get_openai_schema(
    *, strict_mode: bool
) -> FunctionDefinition

The OpenAI schema of the action function.

This method is meant to be used if you want to explicitly pass the strict_mode argument to the get_openai_schema method. Otherwise, you should use the openai_schema property. This is why strict_mode is a required argument.

PARAMETER DESCRIPTION
strict_mode

If True, the schema will be generated in strict mode. If False, the schema will be generated in non-strict mode.

TYPE: bool

RETURNS DESCRIPTION
FunctionDefinition

The OpenAI schema of the action function.

Source code in conatus/actions/action.py
def get_openai_schema(self, *, strict_mode: bool) -> FunctionDefinition:
    """The OpenAI schema of the action function.

    This method is meant to be used if you want to explicitly pass the
    `strict_mode` argument to the `get_openai_schema` method. Otherwise,
    you should use the `openai_schema` property. This is why `strict_mode`
    is a required argument.

    Args:
        strict_mode: If True, the schema will be generated in strict mode.
            If False, the schema will be generated in non-strict mode.

    Returns:
        The OpenAI schema of the action function.
    """
    return generate_openai_json_schema(
        self.json_schema,
        strict_mode=strict_mode,
    )

pretty_function_info

pretty_function_info() -> None

Pretty-print the function information.

Source code in conatus/actions/action.py
def pretty_function_info(self) -> None:  # pragma: no cover
    """Pretty-print the function information."""
    print(self._function_info.pretty_print(colors=True))  # noqa: T201

get_maybe_recipe_retrievability_warnings

get_maybe_recipe_retrievability_warnings(
    i: int, *, with_first_message: bool = True
) -> str | None

Generate a warning message if the action is not retrievable.

Useful when are creating an Agent and want to display the warnings then.

PARAMETER DESCRIPTION
i

The index of the action in the list of actions

TYPE: int

with_first_message

If True, the first message will be included. Defaults to True.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
str | None

The warning message if the action is not retrievable, or None

str | None

otherwise.

Source code in conatus/actions/action.py
def get_maybe_recipe_retrievability_warnings(
    self,
    i: int,
    *,
    with_first_message: bool = True,
) -> str | None:
    """Generate a warning message if the action is not retrievable.

    Useful when are creating an [`Agent`][conatus.agents.Agent]
    and want to display the warnings then.

    Args:
        i: The index of the action in the list of actions
        with_first_message: If True, the first message will be included.
            Defaults to True.

    Returns:
        The warning message if the action is not retrievable, or None
        otherwise.
    """
    msg = (
        (
            "\n⚠️IMPORTANT: You can run the agent, but you will NOT be"
            " able to write a recipe with it.\n"
            "This is because you have at least one action that cannot"
            " be retrieved:\n\n"
        )
        if with_first_message
        else ""
    )
    if isinstance(self.retrievability_info, NonRetrievableActionInfo):
        msg += f"- {self.retrievability_info.origin_qualname} "
        msg += f"({self._to_ith(i)} in your list)\n"
        match self.retrievability_info.reason_why_not_retrievable:
            case "within_locals":
                msg += (
                    "  It is within the locals of a function. \n"
                    "  You will not be able to write a recipe with it "
                    "unless you move it outside of that function."
                )
            case "within_main":  # pragma: no cover
                msg += (
                    "  It is within the main block of a script. \n"
                    "  You will not be able to write a recipe with it "
                    "unless you move it to a file."
                )
            case "changes_not_json_serializable":  # pragma: no branch
                msg += (
                    "  Some of its attributes have changed, and "
                    "these new attributes are not JSON serializable.\n"
                    "  You will not be "
                    "able to write a recipe with it unless you: \n"
                    "    1. Do not perform any changes to the action's "
                    "attributes before passing it to the agent, \n"
                    "    2. If you do need to perform changes,"
                    " make sure "
                    "that the new values are JSON serializable."
                )
        return msg
    return None

get_maybe_recipe_retrievability_instructions

get_maybe_recipe_retrievability_instructions() -> (
    RetrievabilityInstructions | None
)

Get the retrievability instructions for the action.

If the action is not retrievable, returns None.

In general, there should be no surprises with this method. If you have established that the action is retrievable, (by asserting that action.retrievability_info is an instance of RetrievableActionInfo ), then you should be sure that this method will the instructions. Nevertheless, if the JSON serialization of the changes fails, we will raise a TypeError.

RETURNS DESCRIPTION
RetrievabilityInstructions | None

The retrievability instructions for the action, or None if the

RetrievabilityInstructions | None

action is not retrievable.

RAISES DESCRIPTION
TypeError

If the changes are not JSON serializable.

Source code in conatus/actions/action.py
def get_maybe_recipe_retrievability_instructions(
    self,
) -> RetrievabilityInstructions | None:
    """Get the retrievability instructions for the action.

    If the action is not retrievable, returns None.

    In general, there should be no surprises with this method. If you
    have established that the action is retrievable, (by asserting that
    [`action.retrievability_info`
    ][conatus.actions.action.Action.retrievability_info] is an instance of
    [`RetrievableActionInfo`
    ][conatus.actions.retrievability.RetrievableActionInfo]),
    then you should be sure that this method will the instructions.
    Nevertheless, if the JSON serialization of the changes fails,
    we will raise a `TypeError`.

    Returns:
        The retrievability instructions for the action, or None if the
        action is not retrievable.

    Raises:
        TypeError: If the changes are not JSON serializable.
    """
    if isinstance(self.retrievability_info, RetrievableActionInfo):
        try:
            changes_json = json.dumps(self._changes)
        except TypeError as e:
            msg = (
                "Even though the action is marked as retrievable, "
                "we were not able to serialize the changes. "
                "This is a bug, please report it."
            )
            raise TypeError(msg) from e
        return self.retrievability_info.get_retrievability_instructions(
            changes_as_json=changes_json if changes_json != "{}" else None,
        )
    return None

get_docstring

get_docstring(*, include_name: bool = True) -> str

Get the docstring of the action.

This docstring is meant to be used in the AI interfaces.

We convert it to Numpy style because we don't have to deal with the indentation of the parameters.

PARAMETER DESCRIPTION
include_name

Whether to include the name of the action in the docstring. Defaults to True.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
str

The docstring.

TYPE: str

Source code in conatus/actions/action.py
def get_docstring(self, *, include_name: bool = True) -> str:  # noqa: C901
    """Get the docstring of the action.

    This docstring is meant to be used in the AI interfaces.

    We convert it to Numpy style because we don't have to deal with
    the indentation of the parameters.

    Args:
        include_name (bool): Whether to include the name of the action in
            the docstring. Defaults to True.

    Returns:
        str: The docstring.
    """
    function_info = self.function_info
    lines: list[str] = []

    if include_name:
        lines.extend([f"Name: '{function_info.fn_name}'", ""])

    # Description section
    if function_info.description:
        lines.extend(
            ["Description", "-----------", function_info.description, ""]
        )

    # Parameters section
    if function_info.parameters:
        lines.extend(["Parameters", "----------"])
        for param_name, param_info in function_info.parameters.items():
            # Type hint
            lines.append(f"{param_name} : {param_info.type_hint_for_llm}")
            # Description
            if param_info.description:
                desc_lines = param_info.description.strip().split("\n")
                lines.extend(f"    {desc_line}" for desc_line in desc_lines)
            # Default value
            dv = param_info.default_value
            if dv is not None and dv is not Ellipsis:
                lines.append(f"    Default: {dv!r}")
            lines.append("")

    # Returns section
    if isinstance(function_info.returns, ReturnInfo):
        lines.extend(
            [
                "Returns",
                "-------",
                f"{function_info.returns.type_hint_for_llm}",
            ]
        )
        if function_info.returns.description:
            desc_lines = function_info.returns.description.strip().split(
                "\n"
            )
            lines.extend(f"    {line}" for line in desc_lines)
    else:  # list[ReturnInfo]
        lines.extend(["Returns", "-------"])
        for i, return_info in enumerate(function_info.returns):
            lines.append(f"{i} : {return_info.type_hint_for_llm}")
            if return_info.description:
                desc_lines = return_info.description.strip().split("\n")
                lines.extend(f"    {line}" for line in desc_lines)

    # Join everything together into a single docstring
    return "\n".join(lines).rstrip()  # strip trailing whitespace/newlines

add_runtime_state

add_runtime_state(runtime_state: RuntimeState) -> None

Add a pointer to the runtime state to the action.

This is useful if you want to use the runtime state in the action.

PARAMETER DESCRIPTION
runtime_state

The runtime state to add to the action.

TYPE: RuntimeState

Source code in conatus/actions/action.py
def add_runtime_state(self, runtime_state: RuntimeState) -> None:
    """Add a pointer to the runtime state to the action.

    This is useful if you want to use the runtime state in the action.

    Args:
        runtime_state: The runtime state to add to the action.
    """
    self.runtime_state = runtime_state

force_variable_update

force_variable_update(variable: ParamType) -> None

Force a variable update.

An Action, especially passive ones, can force a variable update.

This is useful, for instance, if you want to ensure that a browser screenshot is saved at a given step.

Note that we compare the variable object by checking its id. If the variable object is present multiple times in the runtime state, it will be updated in all of them.

PARAMETER DESCRIPTION
variable

The variable to update.

TYPE: ParamType

Source code in conatus/actions/action.py
def force_variable_update(self, variable: ParamType) -> None:
    """Force a variable update.

    An [`Action`][conatus.actions.action.Action], especially passive
    ones, can force a variable update.

    This is useful, for instance, if you want to ensure that a browser
    screenshot is saved at a given step.

    Note that we compare the variable object by checking its [`id`][id].
    If the variable object is present multiple times in the runtime state,
    it will be updated in all of them.

    Args:
        variable: The variable to update.
    """
    if self.runtime_state is None:
        return
    variable_id = id(variable)
    variable_exists = False
    for var in self.runtime_state.variables.values():
        if id(var.value) == variable_id:
            logger.debug(
                "Updating variable %s with value %s",
                var.name,
                variable,
            )
            variable_exists = True
            _ = var.update(
                value=variable,
                step=self.runtime_state.step_count,
                skip_if_equal=False,
            )
            self.runtime_state.pending_variable_updates.append(var.name)
    if not variable_exists:
        _ = self.runtime_state.add_result(result=variable)

check_params

check_params(
    *args: ParamType, **kwargs: ParamType
) -> dict[str, ParamType]

Check the parameters of the action.

This method is used to check the parameters of the action.

PARAMETER DESCRIPTION
*args

Positional arguments passed to the action function.

TYPE: ParamType DEFAULT: ()

**kwargs

Keyword arguments passed to the action function.

TYPE: ParamType DEFAULT: {}

RETURNS DESCRIPTION
dict[str, ParamType]

A dictionary with the parameters as keys and their values as values.

Source code in conatus/actions/action.py
def check_params(
    self, *args: ParamType, **kwargs: ParamType
) -> dict[str, ParamType]:
    """Check the parameters of the action.

    This method is used to check the parameters of the action.

    Args:
        *args: Positional arguments passed to the action function.
        **kwargs: Keyword arguments passed to the action function.

    Returns:
        A dictionary with the parameters as keys and their values as values.
    """
    return check_params(self.function_info, *args, **kwargs)

is_action_function

is_action_function(func: Callable[Ps, ParamType]) -> bool

Internal function to check if a function is an action function.

To be used in conjunction with @Action.function and mark_action_function

from conatus.actions.action import is_action_function, Action

class MyAction(Action):

    @Action.function
    def my_action(self, a: int, b: int) -> int:
        return a + b

assert is_action_function(MyAction.my_action)  # True
PARAMETER DESCRIPTION
func

The function to check.

TYPE: Callable[Ps, ParamType]

RETURNS DESCRIPTION
bool

True if the function is an action function, False otherwise.

Source code in conatus/actions/action.py
def is_action_function(func: Callable[Ps, ParamType]) -> bool:
    """Internal function to check if a function is an action function.

    To be used in conjunction with [`@Action.function`](
    ../api/action.md#conatus.actions.action.Action.function) and
    [`mark_action_function`][conatus.actions.action.mark_action_function]

    ```python
    from conatus.actions.action import is_action_function, Action

    class MyAction(Action):

        @Action.function
        def my_action(self, a: int, b: int) -> int:
            return a + b

    assert is_action_function(MyAction.my_action)  # True
    ```

    Args:
        func: The function to check.

    Returns:
        True if the function is an action function, False otherwise.
    """
    return hasattr(func, "__is_action_function")

mark_action_function

mark_action_function(func: F) -> F

Internal function to mark a method as an action function.

Can be used in conjunction with is_action_function and @Action.function

from conatus.actions.action import mark_action_function, is_action_function

def a_function(a: int, b: int) -> int:
    return a + b

assert not is_action_function(a_function)  # False
assert is_action_function(mark_action_function(a_function))  # True
Args: func: The function that the action will call.

RETURNS DESCRIPTION
F

The same function that was passed as an argument.

Source code in conatus/actions/action.py
def mark_action_function(func: F) -> F:
    """Internal function to mark a method as an action function.

    Can be used in conjunction with [`is_action_function`](
    ../api/action.md#conatus.actions.action.is_action_function) and
    [`@Action.function`][conatus.actions.action.Action.function]

    ```python
    from conatus.actions.action import mark_action_function, is_action_function

    def a_function(a: int, b: int) -> int:
        return a + b

    assert not is_action_function(a_function)  # False
    assert is_action_function(mark_action_function(a_function))  # True
    ```
    Args:
        func: The function that the action will call.

    Returns:
        The same function that was passed as an argument.
    """
    func.__is_action_function = True  # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue]
    return func

to_snake_case

to_snake_case(name: str) -> str

Convert a camel case string to snake case.

See: https://stackoverflow.com/questions/1175208/

PARAMETER DESCRIPTION
name

The camel case string to convert.

TYPE: str

RETURNS DESCRIPTION
str

The snake case string.

Source code in conatus/actions/action.py
def to_snake_case(name: str) -> str:
    """Convert a camel case string to snake case.

    See: https://stackoverflow.com/questions/1175208/

    Args:
        name: The camel case string to convert.

    Returns:
        The snake case string.
    """
    pattern = re.compile(r"(?<!^)(?=[A-Z])")
    return pattern.sub("_", name).lower()

determine_always_available

determine_always_available(
    function_info: FunctionInfo,
) -> tuple[bool, list[str]]

Determine what the action needs to be available.

PARAMETER DESCRIPTION
function_info

The function info.

TYPE: FunctionInfo

RETURNS DESCRIPTION
bool

True if the action is always available, False otherwise.

list[str]

The discriminator keys if the action is sometimes available, an empty list otherwise.

Source code in conatus/actions/action.py
def determine_always_available(
    function_info: FunctionInfo,
) -> tuple[bool, list[str]]:
    """Determine what the action needs to be available.

    Args:
        function_info: The function info.

    Returns:
        (bool): True if the action is always available, False otherwise.
        (list[str]): The discriminator keys if the action is sometimes
            available, an empty list otherwise.
    """
    always_available = True
    discriminator_keys: list[str] = []
    for param_name, param in function_info.parameters.items():
        is_discriminator = param.is_required and not param.is_json_serializable
        if is_discriminator:
            discriminator_keys.append(f"{function_info.fn_name}${param_name}")
            always_available = False
    return always_available, discriminator_keys

conatus.actions.decorators

Decorators for the Action class.

action

action() -> (
    Callable[[Callable[P, R]], TypedAction[P, R]]
)
action(func: Callable[P, R]) -> TypedAction[P, R]
action(
    func: Callable[P, R],
    /,
    *,
    as_passthrough: bool = False,
    desc: str | None = None,
    override_type_hint_for_llm: bool = False,
    suppress_strict_mode_warning: bool = False,
    passive: bool = False,
    hide_if_computer_use_mode: bool = False,
    force_update_on: str | list[str] | None = None,
) -> TypedAction[P, R]
action(
    *,
    as_passthrough: Literal[False] = False,
    override_type_hint_for_llm: bool = False,
    desc: str | None = None,
    suppress_strict_mode_warning: bool = False,
    passive: bool = False,
    hide_if_computer_use_mode: bool = False,
    force_update_on: str | list[str] | None = None
) -> Callable[[Callable[P, R]], TypedAction[P, R]]
action(
    *,
    as_passthrough: Literal[True],
    desc: str | None = None,
    override_type_hint_for_llm: bool = False,
    suppress_strict_mode_warning: bool = False,
    passive: bool = False,
    hide_if_computer_use_mode: bool = False,
    force_update_on: str | list[str] | None = None
) -> Callable[[Callable[P, R]], Callable[P, R]]
action(
    func: Callable[P, R] | None = None,
    /,
    *,
    as_passthrough: bool = False,
    desc: str | None = None,
    override_type_hint_for_llm: bool = False,
    suppress_strict_mode_warning: bool = False,
    passive: bool = False,
    hide_if_computer_use_mode: bool = False,
    force_update_on: str | list[str] | None = None,
) -> (
    TypedAction[P, R]
    | Callable[[Callable[P, R]], TypedAction[P, R]]
    | Callable[[Callable[P, R]], Callable[P, R]]
)

Decorator to create an Action subclass.

This decorator takes a function and returns an Action in a somewhat transparent way. You can use it as a decorator or a function, with or without arguments.

from conatus.actions.decorators import action

# You can use `action` as a decorator
@action
def multiply_by_two(x: int) -> int:
    return x * 2

# It looks like a normal function, but you get things like type-checking,
# meaning `multiply_by_two("five")` will not work
assert multiply_by_two(5) == 10

# You can also use `action` as a function
def multiply_by_ten(x: int) -> int:
    return x * 10

MultiplyByTen = action(multiply_by_ten)
assert MultiplyByTen(5) == 50

Passthrough mode

Passthrough mode is useful when you want to use the action decorator to transform an instance method into an Action subclass. Doing so is actually tricker than it appears. Consider this:

from dataclasses import dataclass

@dataclass
class MyClass:

    attribute: int = 10

    def my_fn(self, x: int) -> int:
        return x + self.attribute

# You can call the function in two ways:
assert MyClass.my_fn(MyClass(), 5) == 15
assert MyClass(attribute=20).my_fn(5) == 25

The problem is that my_fn has two different signatures based on the context in which it is called. Unfortunately, if we convert my_fn to an Action, the type-checker will lose the ability to understand that nuance. There's really no way around it.

Which means that, if you want to use the @action decorator on an instance method, you have two choices:

  1. You can use @action as is, and the method will be transformed to a TypedAction. But this means that the type-checker will be demand that you pass an instance of your class as the first argument of your method, regardless of whether that method is attached to a live instance. Concretely:

    from conatus.actions.action import TypedAction
    from conatus.actions.decorators import action
    
    @dataclass
    class MyClass:
    
         attribute: int = 10
    
         @action
         def my_fn(self, x: int) -> int:
             return x + self.attribute
    
    
    assert type(MyClass.my_fn) is TypedAction
    # Works fine with your type-checker
    assert MyClass.my_fn(MyClass(), 5) == 15
    
    # This works, but your type checker will want you to write instead:
    # MyClass().my_fn(attribute=20, 5)
    assert MyClass(attribute=20).my_fn(5) == 25
    
  2. You can use @action(as_passthrough=True). This will tell the type checker that the function is returned without any changes, even though it is actually a TypedAction. This will solve the problem above. But it comes with one trade-off: you will not be able to access properties traditionally associated with an Action:

    from conatus.actions.action import TypedAction
    from conatus.actions.decorators import action
    
    @dataclass
    class MyClass:
    
         attribute: int = 10
    
         @action(as_passthrough=True)
         def my_fn(self, x: int) -> int:
             return x + self.attribute
    
    assert type(MyClass.my_fn) is not TypedAction
    # Works fine now with your type-checker
    assert MyClass(attribute=20).my_fn(5) == 25
    # This will not work anymore, since MyClass is a callable
    assert getattr("_function_info", MyClass.my_fn, None) is None
    # But you can retrieve the underlying action as such:
    assert type(getattr("_action", MyClass.my_fn, None)) is TypedAction
    

Because the passthrough mode approach forces us to return something that is not an Action, it is disabled by default, even when we detect that the @action decorator is being used on an instance method.

PARAMETER DESCRIPTION
func

The function that the action will call.

TYPE: Callable[P, R] | None DEFAULT: None

as_passthrough

If True, we will pretend we're returning a function. This is useful if you use the action decorator for an instance method and don't want to have type-checkers yelling at you.

TYPE: bool DEFAULT: False

desc

Description of the action function. If None, the docstring of the function will be used.

TYPE: str | None DEFAULT: None

override_type_hint_for_llm

If True, the type hints of the function will be ignored and replaced by a more LLM-friendly version.

TYPE: bool DEFAULT: False

suppress_strict_mode_warning

If True, the warning about strict mode will be suppressed.

TYPE: bool DEFAULT: False

passive

If True, using the action during a run will not add it to the Recipe. In other words, it's an action that is useful for the LLM to understand the state of the world, but useless for the runtime to execute. One characteristic of passive actions is that they cannot return anything; the only thing they can do is (optionally) print to the screen.

TYPE: bool DEFAULT: False

hide_if_computer_use_mode

If True, the action will be hidden from the AI if it uses computer use mode. For more information, see Action.hide_if_computer_use_mode .

TYPE: bool DEFAULT: False

force_update_on

Name(s) of the input variable(s) that will be checked for changes. For instance, if you have an action that modifies a browser, but the browser is not explicitly returned by the action, the Runtime will account for the fact that the browser has been modified.

TYPE: str | list[str] | None DEFAULT: None

RETURNS DESCRIPTION
TypedAction[P, R] | Callable[[Callable[P, R]], TypedAction[P, R]] | Callable[[Callable[P, R]], Callable[P, R]]

If func is not provided, a function that takes a function and

TypedAction[P, R] | Callable[[Callable[P, R]], TypedAction[P, R]] | Callable[[Callable[P, R]], Callable[P, R]]

returns an Action subclass. If func is provided, an Action

TypedAction[P, R] | Callable[[Callable[P, R]], TypedAction[P, R]] | Callable[[Callable[P, R]], Callable[P, R]]

subclass.

RAISES DESCRIPTION
TypeError

If the func argument is a non-function Callable .

Source code in conatus/actions/decorators.py
def action(  # noqa: C901
    func: Callable[P, R] | None = None,
    /,
    *,
    as_passthrough: bool = False,
    desc: str | None = None,
    override_type_hint_for_llm: bool = False,
    suppress_strict_mode_warning: bool = False,
    passive: bool = False,
    hide_if_computer_use_mode: bool = False,
    force_update_on: str | list[str] | None = None,
) -> (
    TypedAction[P, R]
    | Callable[[Callable[P, R]], TypedAction[P, R]]
    | Callable[[Callable[P, R]], Callable[P, R]]
):
    """Decorator to create an `Action` subclass.

    This decorator takes a function and returns an `Action` in a somewhat
    transparent way. You can use it as a decorator or a function, with or
    without arguments.

    ```python
    from conatus.actions.decorators import action

    # You can use `action` as a decorator
    @action
    def multiply_by_two(x: int) -> int:
        return x * 2

    # It looks like a normal function, but you get things like type-checking,
    # meaning `multiply_by_two("five")` will not work
    assert multiply_by_two(5) == 10

    # You can also use `action` as a function
    def multiply_by_ten(x: int) -> int:
        return x * 10

    MultiplyByTen = action(multiply_by_ten)
    assert MultiplyByTen(5) == 50
    ```

    # Passthrough mode

    Passthrough mode is useful when you want to use the `action` decorator
    to transform an instance method into an `Action` subclass. Doing so is
    actually tricker than it appears. Consider this:

    ```python
    from dataclasses import dataclass

    @dataclass
    class MyClass:

        attribute: int = 10

        def my_fn(self, x: int) -> int:
            return x + self.attribute

    # You can call the function in two ways:
    assert MyClass.my_fn(MyClass(), 5) == 15
    assert MyClass(attribute=20).my_fn(5) == 25
    ```

    The problem is that `my_fn` has two different signatures based on the
    context in which it is called. Unfortunately, if we convert `my_fn` to
    an `Action`, the type-checker will lose the ability to understand that
    nuance. There's really no way around it.

    Which means that, if you want to use the `@action` decorator on an
    instance method, you have two choices:

    1. You can use `@action` as is, and the method will be transformed to
       a `TypedAction`. But this means that the type-checker will be demand
       that you pass an instance of your class as the first argument of your
       method, regardless of whether that method is attached to a live instance.
       Concretely:

       ```python
       from conatus.actions.action import TypedAction
       from conatus.actions.decorators import action

       @dataclass
       class MyClass:

            attribute: int = 10

            @action
            def my_fn(self, x: int) -> int:
                return x + self.attribute


       assert type(MyClass.my_fn) is TypedAction
       # Works fine with your type-checker
       assert MyClass.my_fn(MyClass(), 5) == 15

       # This works, but your type checker will want you to write instead:
       # MyClass().my_fn(attribute=20, 5)
       assert MyClass(attribute=20).my_fn(5) == 25
       ```


    2. You can use `@action(as_passthrough=True)`. This will tell the type
       checker that the function is returned without any changes, even though
       it is actually a `TypedAction`. This will solve the problem above. But
       it comes with one trade-off: you will not be able to access properties
       traditionally associated with an `Action`:

       ```python
       from conatus.actions.action import TypedAction
       from conatus.actions.decorators import action

       @dataclass
       class MyClass:

            attribute: int = 10

            @action(as_passthrough=True)
            def my_fn(self, x: int) -> int:
                return x + self.attribute

       assert type(MyClass.my_fn) is not TypedAction
       # Works fine now with your type-checker
       assert MyClass(attribute=20).my_fn(5) == 25
       # This will not work anymore, since MyClass is a callable
       assert getattr("_function_info", MyClass.my_fn, None) is None
       # But you can retrieve the underlying action as such:
       assert type(getattr("_action", MyClass.my_fn, None)) is TypedAction
       ```

    Because the passthrough mode approach forces us to return something that is
    not an `Action`, it is disabled by default, even when we detect that the
    `@action` decorator is being used on an instance method.

    Args:
        func: The function that the action will call.
        as_passthrough: If True, we will pretend we're returning a function.
            This is useful if you use the `action` decorator for an instance
            method and don't want to have type-checkers yelling at you.
        desc: Description of the action function. If None, the docstring of
            the function will be used.
        override_type_hint_for_llm: If True, the type hints of the function
            will be ignored and replaced by a more LLM-friendly version.
        suppress_strict_mode_warning: If True, the warning about strict mode
            will be suppressed.
        passive: If `True`, using the action during a run will not add it to
            the [`Recipe`][conatus.recipes.recipe.Recipe]. In other words,
            it's an action that is useful for the LLM to understand the state
            of the world, but useless for the runtime to execute. One
            characteristic of passive actions is that they cannot return
            anything; the only thing they can do is (optionally) print to
            the screen.
        hide_if_computer_use_mode: If `True`, the action will be hidden from
            the AI if it uses computer use mode. For more information, see
            [`Action.hide_if_computer_use_mode`
            ][conatus.actions.action.Action.hide_if_computer_use_mode].
        force_update_on: Name(s) of the input variable(s) that will be
            checked for changes. For instance, if you have an action that
            modifies a browser, but the browser is not explicitly returned by
            the action, the [`Runtime`][conatus.runtime.runtime.Runtime] will
            account for the fact that the browser has been modified.

    Returns:
        If `func` is not provided, a function that takes a function and
        returns an `Action` subclass. If `func` is provided, an `Action`
        subclass.

    Raises:
        TypeError: If the `func` argument is a non-function [`Callable`
            ][typing.Callable].
    """

    def wrapper(
        func: Callable[P, R],
        *,
        _privileged: bool = False,
    ) -> TypedAction[P, R]:
        if not isinstance(func, PyFunctionType | MethodType):
            msg = "The `action` decorator can only be used on functions."
            raise TypeError(msg)
        cls_ = TypedAction[P, R]  # type: ignore[valid-type]
        instance = cls_(direct_execute=False)
        instance._intercept_setattr = False
        super(TypedAction, instance).__init__()
        instance._action_function = func
        instance.retrievability_info = ActionBlueprint.process_retrievability(
            origin_type="decorator",
            origin_module=func.__module__,
            origin_qualname=func.__qualname__,
        )
        instance.passive = passive
        instance.runtime_state = None
        instance.hide_if_computer_use_mode = hide_if_computer_use_mode
        instance._function_info = extract_function_info(
            func,
            is_action_function=False,
            override_type_hint_for_llm=override_type_hint_for_llm,
            desc=desc,
        )
        instance.force_update_on = (
            [force_update_on]
            if isinstance(force_update_on, str)
            else force_update_on
            if force_update_on is not None
            else []
        )

        instance._privileged = _privileged
        if not _privileged:
            # This should trigger ActionBlueprint.check_name_is_not_reserved
            instance.name = func.__qualname__.replace(".", "-")
        else:
            instance._name = func.__qualname__.replace(".", "-")
        instance.always_available, instance.discriminator_keys = (
            determine_always_available(instance._function_info)
        )

        instance._pydantic_models = generate_pydantic_models(
            instance._function_info,
            use_test_variables_if_none=True,
        )
        try:
            _ = generate_openai_json_schema(
                instance.json_schema,
                strict_mode=True,
            )
        except OpenAIStrictCastError as e:
            logger.debug(e)
            if not suppress_strict_mode_warning:
                logger.warning(
                    f"{func.__name__} cannot be represented in "
                    "OpenAI's strict mode, so it will be passed to OpenAI "
                    "in non-strict mode. For more information, set the "
                    "`strict_mode` keyword to `True` and read the errors."
                )
            instance.strict_mode = False
        else:
            instance.strict_mode = True

        instance._intercept_setattr = True
        instance._changes = OrderedDict()
        return instance

    if func:
        if isinstance(func, PyFunctionType | MethodType):
            return wrapper(func)
        msg = "The `action` decorator can only be used on functions."
        raise TypeError(msg)
    if as_passthrough:

        def passthrough_wrapper(func: Callable[P, R]) -> Callable[P, R]:
            typed_action = wrapper(func)

            def passthrough_exec(*args: P.args, **kwargs: P.kwargs) -> R:
                return typed_action.execute(*args, **kwargs)

            passthrough_exec.__action = typed_action  # type: ignore[attr-defined] # pyright: ignore[reportFunctionMemberAccess]

            return passthrough_exec

        return passthrough_wrapper
    return wrapper

typeify

typeify(
    func: Callable[Concatenate[X, Ps], R],
) -> TypedAction[Ps, R]

Utility to tell mypy or pyright the type hints of an Action.

This really should be used only if you are struggling or annoyed by type checkers not recognizing the type hints of your Action, and in particular not correctly inferring the return type of the action function.

Note

When to use this function:

  • For pyright: when you've defined a subclass of Action. If you've defined a subclass of TypedAction, this should not be necessary.
  • For mypy: when you've defined a subclass of Action or TypedAction.

Why not the entire Action class, you may ask? This has to do with the type hinting. Only by passing the action function itself will mypy be able to see its arguments and return types. It's not something it can see from the class itself.

from conatus.actions.action import Action, TypedAction
from conatus.actions.decorators import typeify
from typing import Concatenate

# The emojis indicate whether the type checkers recognize the right types

class MyAction(Action):
    @Action.function
    def my_action(self, a: int, b: int) -> int:
        return a + b

result = MyAction(1, 2) # Pyright: 👎, mypy: 👎
typeified = typeify(MyAction.my_action)(1, 2) # Pyright: ✅, mypy: ✅

class MyTypedAction(TypedAction[[int,int], int]):
    @Action.function
    def now_typed(self, a: int, b: int) -> int:
        return a + b

result = MyTypedAction(1, 2) # Pyright: ✅, mypy: 👎
typeified = typeify(MyTypedAction.now_typed)(1, 2) # Pyright: ✅, mypy: ✅

Warning

This function returns an instance of TypedAction, not a class. Most of the time, you won't notice or care, but this might be a cause for bugs.

PARAMETER DESCRIPTION
func

The action function (not the class!)

TYPE: Callable[Concatenate[X, Ps], R]

RETURNS DESCRIPTION
TypedAction[Ps, R]

An instance of the TypedAction class with the correct

TypedAction[Ps, R]

type hints.

RAISES DESCRIPTION
TypeError

If the function is not part of an Action subclass.

TypeError

If the callable is not a function.

Source code in conatus/actions/decorators.py
def typeify(
    func: Callable[Concatenate[X, Ps], R],
) -> TypedAction[Ps, R]:
    """Utility to tell mypy or pyright the type hints of an Action.

    This really should be used only if you are struggling or annoyed by type
    checkers not recognizing the type hints of your `Action`, and in particular
    not correctly inferring the return type of the action function.

    !!! note
        When to use this function:

        - For `pyright`: when you've defined a subclass of `Action`. If you've
        defined a subclass of `TypedAction`, this should not be necessary.

        - For `mypy`: when you've defined a subclass of `Action` or
        `TypedAction`.

    Why not the entire `Action` class, you may ask? This has to do with the
    type hinting. Only by passing the action function itself will mypy be able
    to see its arguments and return types. It's not something it can see from
    the class itself.

    ```python
    from conatus.actions.action import Action, TypedAction
    from conatus.actions.decorators import typeify
    from typing import Concatenate

    # The emojis indicate whether the type checkers recognize the right types

    class MyAction(Action):
        @Action.function
        def my_action(self, a: int, b: int) -> int:
            return a + b

    result = MyAction(1, 2) # Pyright: 👎, mypy: 👎
    typeified = typeify(MyAction.my_action)(1, 2) # Pyright: ✅, mypy: ✅

    class MyTypedAction(TypedAction[[int,int], int]):
        @Action.function
        def now_typed(self, a: int, b: int) -> int:
            return a + b

    result = MyTypedAction(1, 2) # Pyright: ✅, mypy: 👎
    typeified = typeify(MyTypedAction.now_typed)(1, 2) # Pyright: ✅, mypy: ✅
    ```

    !!! warning
        This function returns an *instance* of `TypedAction`, not a class.
        Most of the time, you won't notice or care, but this might be a
        cause for bugs.

    Args:
        func: The action function (not the class!)

    Returns:
        An instance of the `TypedAction` class with the correct
        type hints.

    Raises:
        TypeError: If the function is not part of an `Action` subclass.
        TypeError: If the callable is not a function.
    """
    if not isinstance(func, PyFunctionType):
        msg = f"The callable must be a function, not {type(func)}"
        raise TypeError(msg)

    qualname_list = func.__qualname__.split(".")
    if len(qualname_list) <= 1:
        msg = (
            "The function cannot be a top-level function."
            " `typeify` should be used only on methods of an `Action`"
            " subclass."
        )
        raise TypeError(msg)

    maybe_action = cast("type", func.__globals__[qualname_list[0]])
    if not issubclass(maybe_action, Action):
        msg = (
            f"The function must be part of an Action subclass, "
            f"not {maybe_action}"
        )
        raise TypeError(msg)
    return cast("TypedAction[Ps, R]", maybe_action(direct_execute=False))

termination_action

termination_action(
    action: Action,
    expected_outputs: OrderedDict[
        str, tuple[TypeOfType, str]
    ],
) -> Action

Decorator to mark an action as a termination action.

This means that we replace all of the input parameters of the action with the expected outputs of the run (or recipe).

Note that at least one of the expected outputs must be a bool named "success".

PARAMETER DESCRIPTION
action

The action to mark as a termination action.

TYPE: Action

expected_outputs

The expected outputs of the run (or recipe). The keys are the names of the outputs, and the values are a tuple containing the type of the output and its description. The inputs of the action will be replaced by these outputs.

TYPE: OrderedDict[str, tuple[TypeOfType, str]]

RETURNS DESCRIPTION
Action

The action with the inputs replaced by the expected outputs.

Source code in conatus/actions/decorators.py
def termination_action(
    action: Action,
    expected_outputs: OrderedDict[str, tuple[TypeOfType, str]],
) -> Action:
    """Decorator to mark an action as a termination action.

    This means that we replace all of the input parameters of the action
    with the expected outputs of the run (or recipe).

    Note that at least one of the expected outputs must be a `bool` named
    "success".

    Args:
        action: The action to mark as a termination action.
        expected_outputs: The expected outputs of the run (or recipe). The
            keys are the names of the outputs, and the values are a tuple
            containing the type of the output and its description. The inputs
            of the action will be replaced by these outputs.

    Returns:
        The action with the inputs replaced by the expected outputs.
    """
    new_params: OrderedDict[str, ParamInfo] = OrderedDict()
    expected_outputs = expected_outputs.copy()
    expected_outputs["success"] = (
        bool,
        "Whether the execution was successful. This will not be "
        "passed to the user.",
    )
    for name, (type_, description) in expected_outputs.items():
        is_json_serializable, json_serializable_subtype = (
            is_json_serializable_type(type_)
        )
        new_params[name] = ParamInfo(
            type_hint=type_,
            type_hint_for_llm=process_typehint(type_, return_type="str"),
            description=description,
            default_value=...,
            kind=ParamKind.POSITIONAL_OR_KEYWORD,
            is_required=True,
            is_json_serializable=is_json_serializable,
            json_serializable_subtype=json_serializable_subtype,
        )
    action.is_termination_action = True
    action._function_info.parameters = new_params
    action.always_available, action.discriminator_keys = (
        determine_always_available(action._function_info)
    )
    action._pydantic_models = generate_pydantic_models(
        action._function_info,
        use_test_variables_if_none=True,
    )
    try:
        _ = generate_openai_json_schema(
            action.json_schema,
            strict_mode=True,
        )
    except OpenAIStrictCastError:
        action.strict_mode = False
    else:
        action.strict_mode = True
    return action

convert_to_action

convert_to_action(
    raw_action: Action | ActionBlueprint | FunctionType,
) -> Action

Convert a raw action to an action.

PARAMETER DESCRIPTION
raw_action

The raw action to convert.

TYPE: Action | ActionBlueprint | FunctionType

RETURNS DESCRIPTION
Action

The action.

Source code in conatus/actions/decorators.py
def convert_to_action(
    raw_action: Action | ActionBlueprint | FunctionType,
) -> Action:
    """Convert a raw action to an action.

    Args:
        raw_action: The raw action to convert.

    Returns:
        The action.
    """
    match Action.is_action(raw_action):
        case "instance":
            return cast("Action", raw_action)
        case "class":
            return cast("Action", raw_action(direct_execute=False))
        case "none":  # pragma: no branch
            return action(raw_action)