File size: 10,454 Bytes
0f6657d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""
AutoAction - Automatic Action Class Selection
==============================================

AutoAction provides a HuggingFace-style API for automatically retrieving the
correct Action class from installed packages or HuggingFace Hub.

This module simplifies working with environment actions by automatically
detecting and returning the appropriate Action class without requiring
manual imports.

Example:
    >>> from openenv import AutoEnv, AutoAction
    >>>
    >>> # Get Action class from environment name
    >>> CodeAction = AutoAction.from_env("coding")
    >>> action = CodeAction(code="print('Hello!')")
    >>>
    >>> # From HuggingFace Hub
    >>> CodeAction = AutoAction.from_env("meta-pytorch/coding-env")
    >>>
    >>> # Use with AutoEnv
    >>> env = AutoEnv.from_env("coding-env")
    >>> result = env.step(action)
"""

from __future__ import annotations

import logging
from typing import Any, Dict, Type

from ._discovery import _is_hub_url, get_discovery
from .auto_env import AutoEnv

logger = logging.getLogger(__name__)


class AutoAction:
    """
    AutoAction automatically retrieves the correct Action class based on
    environment names or HuggingFace Hub repositories.

    This class follows the HuggingFace AutoModel pattern, making it easy to
    get the right Action class without needing to know which module to import.

    The class provides factory methods that look up the Action class and
    return the class (not an instance) for you to instantiate.

    Example:
        >>> # From installed package
        >>> CodeAction = AutoAction.from_env("coding")
        >>> action = CodeAction(code="print('test')")
        >>>
        >>> # From HuggingFace Hub
        >>> CodeAction = AutoAction.from_env("meta-pytorch/coding-env")
        >>> action = CodeAction(code="print('test')")
        >>>
        >>> # Use with AutoEnv for a complete workflow
        >>> env = AutoEnv.from_env("coding-env")
        >>> ActionClass = AutoAction.from_env("coding-env")
        >>> action = ActionClass(code="print('Hello, AutoAction!')")
        >>> result = env.step(action)

    Note:
        AutoAction is not meant to be instantiated directly. Use the class
        method from_env() instead.
    """

    def __init__(self):
        """AutoAction should not be instantiated directly. Use class methods instead."""
        raise TypeError(
            "AutoAction is a factory class and should not be instantiated directly. "
            "Use AutoAction.from_hub() or AutoAction.from_env() instead."
        )

    @classmethod
    def from_env(cls, name: str, skip_install: bool = False) -> Type:
        """
        Get the Action class from environment name or HuggingFace Hub repository.

        This method automatically:
        1. Checks if the name is a HuggingFace Hub URL/repo ID
        2. If Hub: downloads and installs the environment package
        3. If local: looks up the installed openenv-* package
        4. Imports and returns the Action class

        Args:
            name: Environment name or HuggingFace Hub repo ID
                  Examples:
                  - "coding" / "coding-env" / "coding_env"
                  - "meta-pytorch/coding-env" (Hub repo ID)
                  - "https://huggingface.co/meta-pytorch/coding-env" (Hub URL)
            skip_install: If True, skip package installation and return
                GenericAction class instead. Use this when working with
                GenericEnvClient to avoid installing remote packages.

        Returns:
            Action class (not an instance!). Returns GenericAction when
            skip_install=True.

        Raises:
            ValueError: If environment not found (only when skip_install=False)
            ImportError: If environment package is not installed (only when skip_install=False)

        Examples:
            >>> # From installed package
            >>> CodeAction = AutoAction.from_env("coding-env")
            >>> action = CodeAction(code="print('Hello!')")
            >>>
            >>> # From HuggingFace Hub
            >>> CodeAction = AutoAction.from_env("meta-pytorch/coding-env")
            >>> action = CodeAction(code="print('Hello!')")
            >>>
            >>> # Skip installation, use GenericAction (for GenericEnvClient)
            >>> ActionClass = AutoAction.from_env("user/repo", skip_install=True)
            >>> action = ActionClass(code="print('Hello!')")  # Returns GenericAction
            >>>
            >>> # Different name formats
            >>> EchoAction = AutoAction.from_env("echo")
            >>> EchoAction = AutoAction.from_env("echo-env")
            >>> EchoAction = AutoAction.from_env("echo_env")
        """
        # If skip_install is True, return GenericAction without any package lookup
        if skip_install:
            from openenv.core.generic_client import GenericAction

            logger.info(
                f"Returning GenericAction for '{name}' (skip_install=True). "
                f"Use keyword arguments to create actions: GenericAction(code='...')"
            )
            return GenericAction

        # Check if it's a HuggingFace Hub URL or repo ID
        if _is_hub_url(name):
            # Ensure package is installed (reuse AutoEnv logic, downloads only if needed)
            env_name = AutoEnv._ensure_package_from_hub(name)
        else:
            env_name = name

        # Get environment info from discovery
        discovery = get_discovery()
        env_info = discovery.get_environment_by_name(env_name)

        if not env_info:
            # Environment not found - provide helpful error message
            available_envs = discovery.discover()

            if not available_envs:
                raise ValueError(
                    "No OpenEnv environments found.\n"
                    "Install an environment with: pip install openenv-<env-name>\n"
                    "Or specify a HuggingFace Hub repository: AutoAction.from_env('openenv/echo_env')"
                )

            # Try to suggest similar environment names
            from difflib import get_close_matches

            env_keys = list(available_envs.keys())
            suggestions = get_close_matches(env_name, env_keys, n=3, cutoff=0.6)

            error_msg = f"Unknown environment '{env_name}'.\n"
            if suggestions:
                error_msg += f"Did you mean: {', '.join(suggestions)}?\n"
            error_msg += f"Available environments: {', '.join(sorted(env_keys))}"

            raise ValueError(error_msg)

        # Get the action class
        try:
            action_class = env_info.get_action_class()
            return action_class
        except ImportError as e:
            raise ImportError(
                f"Failed to import action class for '{env_name}'.\n"
                f"Package '{env_info.package_name}' appears to be installed but the module cannot be imported.\n"
                f"Try reinstalling: pip install --force-reinstall {env_info.package_name}\n"
                f"Original error: {e}"
            ) from e

    @classmethod
    def from_hub(cls, env_name: str, skip_install: bool = False) -> Type:
        """
        Get the Action class from environment name.

        This is an alias for from_env() for backward compatibility and clarity.

        Args:
            env_name: Environment name (e.g., "coding", "echo")
            skip_install: If True, skip package installation and return
                GenericAction class instead.

        Returns:
            Action class (not an instance!)

        Examples:
            >>> CodeAction = AutoAction.from_hub("coding")
            >>> action = CodeAction(code="print('Hello!')")
        """
        return cls.from_env(env_name, skip_install=skip_install)

    @classmethod
    def get_action_info(cls, name: str) -> Dict[str, Any]:
        """
        Get detailed information about an action class.

        Args:
            name: Environment name

        Returns:
            Dictionary with action class metadata

        Raises:
            ValueError: If environment not found

        Examples:
            >>> info = AutoAction.get_action_info("coding")
            >>> print(info['action_class'])
            'CodingAction'
            >>> print(info['module'])
            'coding_env.client'
        """
        discovery = get_discovery()
        env_info = discovery.get_environment_by_name(name)

        if not env_info:
            raise ValueError(f"Unknown environment: {name}")

        return {
            "env_key": env_info.env_key,
            "env_name": env_info.name,
            "package": env_info.package_name,
            "action_class": env_info.action_class_name,
            "observation_class": env_info.observation_class_name,
            "module": env_info.client_module_path,
        }

    @classmethod
    def list_actions(cls) -> None:
        """
        Print a formatted list of all available action classes.

        This discovers all installed openenv-* packages and displays
        their action class information in a user-friendly format.

        Examples:
            >>> AutoAction.list_actions()
            Available Action Classes:
            ----------------------------------------------------------------------
              echo           : EchoAction (from openenv-echo-env)
              coding         : CodingAction (from openenv-coding_env)
            ----------------------------------------------------------------------
            Total: 2 action classes
        """
        discovery = get_discovery()
        environments = discovery.discover()

        print("Available Action Classes:")
        print("-" * 70)

        if not environments:
            print("  No OpenEnv environments found.")
            print("  Install environments with: pip install openenv-<env-name>")
        else:
            for env_key in sorted(environments.keys()):
                env = environments[env_key]
                print(f"  {env_key:<15}: {env.action_class_name}")
                print(f"                   Package: {env.package_name}")

        print("-" * 70)
        print(f"Total: {len(environments)} action classes")