openenv-agent 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Croma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: openenv-agent
3
+ Version: 0.1.0
4
+ Summary: Agent client for OpenEnv environments - connects RL agents to OpenEnv servers for inference and training
5
+ Author: Croma
6
+ License: MIT
7
+ Keywords: openenv,reinforcement-learning,agent,rl,content-moderation
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: openai>=1.3.0
19
+ Requires-Dist: httpx>=0.25.0
20
+ Requires-Dist: pyyaml>=6.0
21
+ Requires-Dist: click>=8.0.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0; extra == "dev"
24
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
25
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # OpenEnv Agent
29
+
30
+ Python client library for connecting RL agents to OpenEnv servers.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install openenv-agent
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ ```python
41
+ from openenv_agent import OpenEnvClient, ModerationAgent
42
+
43
+ # Connect to an OpenEnv server
44
+ client = OpenEnvClient(base_url="http://localhost:8000")
45
+
46
+ # Reset environment
47
+ obs = client.reset()
48
+
49
+ # Use the moderation agent
50
+ agent = ModerationAgent()
51
+ action = agent.predict(obs)
52
+
53
+ # Step the environment
54
+ next_obs, reward, done, info = client.step(action)
55
+ ```
56
+
57
+ ## CLI Usage
58
+
59
+ ```bash
60
+ # Run agent against a server
61
+ openenv-agent run http://localhost:8000
62
+
63
+ # Interactive mode
64
+ openenv-agent interactive http://localhost:8000
65
+
66
+ # Evaluate on a dataset
67
+ openenv-agent eval http://localhost:8000 --dataset ./data.json
68
+ ```
69
+
70
+ ## Features
71
+
72
+ - Async/sync OpenEnv client with Gymnasium-style API
73
+ - Built-in ModerationAgent for content moderation environments
74
+ - Environment loader from openenv.yaml configs
75
+ - CLI tool for easy server interaction
@@ -0,0 +1,48 @@
1
+ # OpenEnv Agent
2
+
3
+ Python client library for connecting RL agents to OpenEnv servers.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install openenv-agent
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from openenv_agent import OpenEnvClient, ModerationAgent
15
+
16
+ # Connect to an OpenEnv server
17
+ client = OpenEnvClient(base_url="http://localhost:8000")
18
+
19
+ # Reset environment
20
+ obs = client.reset()
21
+
22
+ # Use the moderation agent
23
+ agent = ModerationAgent()
24
+ action = agent.predict(obs)
25
+
26
+ # Step the environment
27
+ next_obs, reward, done, info = client.step(action)
28
+ ```
29
+
30
+ ## CLI Usage
31
+
32
+ ```bash
33
+ # Run agent against a server
34
+ openenv-agent run http://localhost:8000
35
+
36
+ # Interactive mode
37
+ openenv-agent interactive http://localhost:8000
38
+
39
+ # Evaluate on a dataset
40
+ openenv-agent eval http://localhost:8000 --dataset ./data.json
41
+ ```
42
+
43
+ ## Features
44
+
45
+ - Async/sync OpenEnv client with Gymnasium-style API
46
+ - Built-in ModerationAgent for content moderation environments
47
+ - Environment loader from openenv.yaml configs
48
+ - CLI tool for easy server interaction
@@ -0,0 +1,43 @@
1
+ [project]
2
+ name = "openenv-agent"
3
+ version = "0.1.0"
4
+ description = "Agent client for OpenEnv environments - connects RL agents to OpenEnv servers for inference and training"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = {text = "MIT"}
8
+ authors = [
9
+ {name = "Croma"}
10
+ ]
11
+ keywords = ["openenv", "reinforcement-learning", "agent", "rl", "content-moderation"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ ]
21
+ dependencies = [
22
+ "openai>=1.3.0",
23
+ "httpx>=0.25.0",
24
+ "pyyaml>=6.0",
25
+ "click>=8.0.0",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=7.0",
31
+ "pytest-asyncio>=0.21.0",
32
+ "ruff>=0.1.0",
33
+ ]
34
+
35
+ [project.scripts]
36
+ openenv-agent = "openenv_agent.cli:main"
37
+
38
+ [build-system]
39
+ requires = ["setuptools>=61.0", "wheel"]
40
+ build-backend = "setuptools.build_meta"
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,14 @@
1
+ """OpenEnv Agent - Client library for OpenEnv RL environments."""
2
+
3
+ from openenv_agent.client import OpenEnvClient
4
+ from openenv_agent.agent import BaseAgent
5
+ from openenv_agent.moderation_agent import ModerationAgent
6
+ from openenv_agent.env_loader import load_env_from_yaml
7
+
8
+ __version__ = "0.1.0"
9
+ __all__ = [
10
+ "OpenEnvClient",
11
+ "BaseAgent",
12
+ "ModerationAgent",
13
+ "load_env_from_yaml",
14
+ ]
@@ -0,0 +1,64 @@
1
+ """Base agent class for OpenEnv."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from typing import Any, Dict, List, Optional
6
+
7
+
8
+ @dataclass
9
+ class AgentConfig:
10
+ """Configuration for an agent."""
11
+ name: str = "BaseAgent"
12
+ temperature: float = 0.0
13
+ max_tokens: int = 200
14
+ timeout: float = 10.0
15
+
16
+
17
+ class BaseAgent(ABC):
18
+ """
19
+ Abstract base class for OpenEnv agents.
20
+
21
+ Subclasses must implement the `predict` method.
22
+ """
23
+
24
+ def __init__(self, config: Optional[AgentConfig] = None):
25
+ """
26
+ Initialize the agent.
27
+
28
+ Args:
29
+ config: Agent configuration
30
+ """
31
+ self.config = config or AgentConfig()
32
+
33
+ @abstractmethod
34
+ def predict(self, observation: Dict[str, Any]) -> Dict[str, Any]:
35
+ """
36
+ Given an observation, return an action.
37
+
38
+ Args:
39
+ observation: Environment observation dict
40
+
41
+ Returns:
42
+ Action dict to send to the environment
43
+ """
44
+ pass
45
+
46
+ def reset(self):
47
+ """Reset agent state between episodes."""
48
+ pass
49
+
50
+ def train(self, episode_data: List[Dict[str, Any]]) -> Dict[str, float]:
51
+ """
52
+ Train the agent on episode data.
53
+
54
+ Args:
55
+ episode_data: List of (observation, action, reward, done, info) tuples
56
+
57
+ Returns:
58
+ Dict of training metrics
59
+ """
60
+ # Default implementation - override for actual training
61
+ return {"status": "no-op"}
62
+
63
+ def __repr__(self) -> str:
64
+ return f"{self.__class__.__name__}(name={self.config.name})"
@@ -0,0 +1,170 @@
1
+ """CLI for openenv-agent."""
2
+
3
+ import sys
4
+ import logging
5
+ from typing import Optional
6
+
7
+ import click
8
+
9
+ from openenv_agent.client import OpenEnvClient
10
+ from openenv_agent.moderation_agent import ModerationAgent
11
+
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
15
+ )
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @click.group()
20
+ @click.version_option(version="0.1.0")
21
+ def main():
22
+ """OpenEnv Agent - Connect RL agents to OpenEnv servers."""
23
+ pass
24
+
25
+
26
+ @main.command()
27
+ @click.argument("url")
28
+ @click.option("--max-steps", type=int, default=None, help="Max steps to run")
29
+ def run(url: str, max_steps: Optional[int]):
30
+ """Run the moderation agent against an OpenEnv server."""
31
+ client = OpenEnvClient(base_url=url)
32
+ agent = ModerationAgent()
33
+
34
+ click.echo(f"[START] Connecting to {url}")
35
+
36
+ try:
37
+ obs = client.reset()
38
+ click.echo(f"[RESET] Initial observation received")
39
+
40
+ step = 0
41
+ total_reward = 0.0
42
+
43
+ while True:
44
+ if max_steps and step >= max_steps:
45
+ break
46
+
47
+ action = agent.predict(obs)
48
+ next_obs, reward, done, info = client.step(action)
49
+
50
+ total_reward += reward
51
+ step += 1
52
+
53
+ click.echo(
54
+ f"[STEP] step={step} action={action['decision']} "
55
+ f"reward={reward:.2f} done={done}"
56
+ )
57
+
58
+ if done:
59
+ break
60
+
61
+ obs = next_obs
62
+
63
+ avg_reward = total_reward / max(step, 1)
64
+ click.echo(f"[END] steps={step} avg_reward={avg_reward:.4f}")
65
+
66
+ except Exception as e:
67
+ click.echo(f"[ERROR] {e}", err=True)
68
+ sys.exit(1)
69
+ finally:
70
+ client.close()
71
+
72
+
73
+ @main.command()
74
+ @click.argument("url")
75
+ def interactive(url: str):
76
+ """Run the agent in interactive debug mode."""
77
+ client = OpenEnvClient(base_url=url)
78
+ agent = ModerationAgent()
79
+
80
+ click.echo(f"Interactive mode - connecting to {url}")
81
+ click.echo("Press Ctrl+C to exit\n")
82
+
83
+ try:
84
+ obs = client.reset()
85
+ click.echo(f"Initial observation:\n{obs}\n")
86
+
87
+ step = 0
88
+ while True:
89
+ click.echo(f"--- Step {step} ---")
90
+ action = agent.predict(obs)
91
+ click.echo(f"Action: {action}")
92
+
93
+ next_obs, reward, done, info = client.step(action)
94
+ click.echo(f"Reward: {reward:.2f}, Done: {done}")
95
+
96
+ if done:
97
+ click.echo("Episode complete!")
98
+ break
99
+
100
+ if click.confirm("Continue?"):
101
+ obs = next_obs
102
+ step += 1
103
+ else:
104
+ break
105
+
106
+ except KeyboardInterrupt:
107
+ click.echo("\nExiting...")
108
+ finally:
109
+ client.close()
110
+
111
+
112
+ @main.command()
113
+ @click.argument("url")
114
+ @click.option("--dataset", type=click.Path(), help="Path to dataset JSON")
115
+ def eval(url: str, dataset: Optional[str]):
116
+ """Evaluate the agent on a dataset."""
117
+ client = OpenEnvClient(base_url=url)
118
+ agent = ModerationAgent()
119
+
120
+ click.echo(f"[EVAL] Starting evaluation against {url}")
121
+
122
+ total_reward = 0.0
123
+ total_steps = 0
124
+ episodes = 0
125
+
126
+ try:
127
+ while True:
128
+ obs = client.reset()
129
+ step = 0
130
+ episode_reward = 0.0
131
+
132
+ while True:
133
+ action = agent.predict(obs)
134
+ next_obs, reward, done, info = client.step(action)
135
+ episode_reward += reward
136
+ step += 1
137
+
138
+ if done:
139
+ break
140
+
141
+ obs = next_obs
142
+
143
+ total_reward += episode_reward
144
+ total_steps += step
145
+ episodes += 1
146
+
147
+ click.echo(f"[EPISODE] {episodes}: steps={step} reward={episode_reward:.2f}")
148
+
149
+ # In real eval, would check against ground truth here
150
+ if dataset:
151
+ # TODO: evaluate against dataset
152
+ pass
153
+
154
+ if episodes >= 10: # Eval on 10 episodes by default
155
+ break
156
+
157
+ click.echo(f"\n[EVAL COMPLETE]")
158
+ click.echo(f"Episodes: {episodes}")
159
+ click.echo(f"Total steps: {total_steps}")
160
+ click.echo(f"Average reward: {total_reward / episodes:.4f}")
161
+
162
+ except Exception as e:
163
+ click.echo(f"[ERROR] {e}", err=True)
164
+ sys.exit(1)
165
+ finally:
166
+ client.close()
167
+
168
+
169
+ if __name__ == "__main__":
170
+ main()
@@ -0,0 +1,209 @@
1
+ """OpenEnv client for connecting to OpenEnv servers."""
2
+
3
+ import json
4
+ import logging
5
+ from dataclasses import dataclass
6
+ from typing import Any, Optional, Tuple
7
+
8
+ import httpx
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @dataclass
14
+ class EnvResponse:
15
+ """Response from an OpenEnv environment step."""
16
+ observation: dict
17
+ reward: float
18
+ done: bool
19
+ info: dict
20
+
21
+
22
+ class OpenEnvClient:
23
+ """
24
+ Client for connecting to OpenEnv servers.
25
+
26
+ Provides a Gymnasium-style API (reset, step, state) for interacting
27
+ with OpenEnv RL environments over HTTP.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ base_url: str,
33
+ timeout: float = 30.0,
34
+ api_key: Optional[str] = None,
35
+ ):
36
+ """
37
+ Initialize the OpenEnv client.
38
+
39
+ Args:
40
+ base_url: Base URL of the OpenEnv server (e.g., "http://localhost:8000")
41
+ timeout: Request timeout in seconds
42
+ api_key: Optional API key for authentication
43
+ """
44
+ self.base_url = base_url.rstrip("/")
45
+ self.timeout = timeout
46
+ self._client = httpx.Client(timeout=timeout)
47
+ if api_key:
48
+ self._client.headers["Authorization"] = f"Bearer {api_key}"
49
+
50
+ def reset(self) -> dict:
51
+ """
52
+ Reset the environment and return the initial observation.
53
+
54
+ Returns:
55
+ dict: Initial observation from the environment
56
+ """
57
+ try:
58
+ response = self._client.post(f"{self.base_url}/reset")
59
+ response.raise_for_status()
60
+ data = response.json()
61
+ return data.get("observation", data)
62
+ except httpx.HTTPError as e:
63
+ logger.error(f"Failed to reset environment: {e}")
64
+ raise
65
+
66
+ def step(self, action: Any) -> Tuple[dict, float, bool, dict]:
67
+ """
68
+ Execute an action in the environment.
69
+
70
+ Args:
71
+ action: Action to take (dict or object with as_dict method)
72
+
73
+ Returns:
74
+ Tuple of (observation, reward, done, info)
75
+ """
76
+ if hasattr(action, "as_dict"):
77
+ action_dict = action.as_dict()
78
+ elif isinstance(action, dict):
79
+ action_dict = action
80
+ else:
81
+ raise ValueError(f"Action must be dict or have as_dict() method, got {type(action)}")
82
+
83
+ try:
84
+ response = self._client.post(
85
+ f"{self.base_url}/step",
86
+ json={"action": action_dict},
87
+ )
88
+ response.raise_for_status()
89
+ data = response.json()
90
+ return (
91
+ data.get("observation", {}),
92
+ data.get("reward", 0.0),
93
+ data.get("done", False),
94
+ data.get("info", {}),
95
+ )
96
+ except httpx.HTTPError as e:
97
+ logger.error(f"Failed to step environment: {e}")
98
+ raise
99
+
100
+ def state(self) -> dict:
101
+ """
102
+ Get the current state of the environment without taking an action.
103
+
104
+ Returns:
105
+ dict: Current environment state
106
+ """
107
+ try:
108
+ response = self._client.get(f"{self.base_url}/state")
109
+ response.raise_for_status()
110
+ return response.json()
111
+ except httpx.HTTPError as e:
112
+ logger.error(f"Failed to get state: {e}")
113
+ raise
114
+
115
+ def health(self) -> bool:
116
+ """
117
+ Check if the environment server is healthy.
118
+
119
+ Returns:
120
+ bool: True if server is healthy, False otherwise
121
+ """
122
+ try:
123
+ response = self._client.get(f"{self.base_url}/health")
124
+ return response.status_code == 200
125
+ except httpx.HTTPError:
126
+ return False
127
+
128
+ def close(self):
129
+ """Close the HTTP client."""
130
+ self._client.close()
131
+
132
+ def __enter__(self):
133
+ return self
134
+
135
+ def __exit__(self, exc_type, exc_val, exc_tb):
136
+ self.close()
137
+ return False
138
+
139
+
140
+ class AsyncOpenEnvClient:
141
+ """
142
+ Async client for connecting to OpenEnv servers.
143
+ """
144
+
145
+ def __init__(
146
+ self,
147
+ base_url: str,
148
+ timeout: float = 30.0,
149
+ api_key: Optional[str] = None,
150
+ ):
151
+ self.base_url = base_url.rstrip("/")
152
+ self.timeout = timeout
153
+ self._client = httpx.AsyncClient(timeout=timeout)
154
+ if api_key:
155
+ self._client.headers["Authorization"] = f"Bearer {api_key}"
156
+
157
+ async def reset(self) -> dict:
158
+ try:
159
+ response = await self._client.post(f"{self.base_url}/reset")
160
+ response.raise_for_status()
161
+ data = response.json()
162
+ return data.get("observation", data)
163
+ except httpx.HTTPError as e:
164
+ logger.error(f"Failed to reset environment: {e}")
165
+ raise
166
+
167
+ async def step(self, action: Any) -> Tuple[dict, float, bool, dict]:
168
+ if hasattr(action, "as_dict"):
169
+ action_dict = action.as_dict()
170
+ elif isinstance(action, dict):
171
+ action_dict = action
172
+ else:
173
+ raise ValueError(f"Action must be dict or have as_dict() method, got {type(action)}")
174
+
175
+ try:
176
+ response = await self._client.post(
177
+ f"{self.base_url}/step",
178
+ json={"action": action_dict},
179
+ )
180
+ response.raise_for_status()
181
+ data = response.json()
182
+ return (
183
+ data.get("observation", {}),
184
+ data.get("reward", 0.0),
185
+ data.get("done", False),
186
+ data.get("info", {}),
187
+ )
188
+ except httpx.HTTPError as e:
189
+ logger.error(f"Failed to step environment: {e}")
190
+ raise
191
+
192
+ async def state(self) -> dict:
193
+ try:
194
+ response = await self._client.get(f"{self.base_url}/state")
195
+ response.raise_for_status()
196
+ return response.json()
197
+ except httpx.HTTPError as e:
198
+ logger.error(f"Failed to get state: {e}")
199
+ raise
200
+
201
+ async def aclose(self):
202
+ await self._client.aclose()
203
+
204
+ async def __aenter__(self):
205
+ return self
206
+
207
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
208
+ await self.aclose()
209
+ return False
@@ -0,0 +1,76 @@
1
+ """Utility for loading OpenEnv environments from YAML config."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Optional
6
+
7
+ import yaml
8
+
9
+ from openenv_agent.client import OpenEnvClient
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def load_env_from_yaml(
15
+ yaml_path: str | Path,
16
+ **client_kwargs
17
+ ) -> Dict[str, Any]:
18
+ """
19
+ Load an OpenEnv environment configuration from a YAML file.
20
+
21
+ Args:
22
+ yaml_path: Path to openenv.yaml
23
+ **client_kwargs: Additional arguments for OpenEnvClient
24
+
25
+ Returns:
26
+ Dict with keys:
27
+ - client: OpenEnvClient instance
28
+ - config: Parsed environment config
29
+ - schemas: Observation and action schemas
30
+ """
31
+ yaml_path = Path(yaml_path)
32
+ if not yaml_path.exists():
33
+ raise FileNotFoundError(f"YAML file not found: {yaml_path}")
34
+
35
+ with open(yaml_path) as f:
36
+ config = yaml.safe_load(f)
37
+
38
+ env_config = config.get("environment", {})
39
+ api_config = config.get("api", {})
40
+ environment_config = config.get("environment_config", {})
41
+ schemas = config.get("schemas", {})
42
+
43
+ # Build base URL
44
+ port = api_config.get("port", 8000)
45
+ base_url = f"http://localhost:{port}"
46
+
47
+ client = OpenEnvClient(base_url=base_url, **client_kwargs)
48
+
49
+ return {
50
+ "client": client,
51
+ "config": {
52
+ "name": env_config.get("name", "unknown"),
53
+ "version": env_config.get("version", "unknown"),
54
+ "description": env_config.get("description", ""),
55
+ },
56
+ "environment_config": environment_config,
57
+ "schemas": schemas,
58
+ "metadata": config.get("metadata", {}),
59
+ }
60
+
61
+
62
+ def load_env_from_url(
63
+ base_url: str,
64
+ api_key: Optional[str] = None
65
+ ) -> OpenEnvClient:
66
+ """
67
+ Load an OpenEnv environment from a URL.
68
+
69
+ Args:
70
+ base_url: Base URL of the OpenEnv server
71
+ api_key: Optional API key
72
+
73
+ Returns:
74
+ OpenEnvClient instance
75
+ """
76
+ return OpenEnvClient(base_url=base_url, api_key=api_key)
@@ -0,0 +1,192 @@
1
+ """Content moderation agent using OpenAI API."""
2
+
3
+ import os
4
+ import logging
5
+ from typing import Any, Dict, Optional
6
+
7
+ from openai import OpenAI, APIError, RateLimitError
8
+
9
+ from openenv_agent.agent import BaseAgent, AgentConfig
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ # Valid decisions and categories from OpenEnv schema
15
+ VALID_DECISIONS = ["ALLOW", "FLAG", "REMOVE", "ESCALATE"]
16
+ VALID_CATEGORIES = ["SPAM", "HATE_SPEECH", "MISINFORMATION", "HARASSMENT", "SAFE"]
17
+
18
+ DECISION_MAP = {
19
+ "allow": "ALLOW",
20
+ "flag": "FLAG",
21
+ "remove": "REMOVE",
22
+ "escalate": "ESCALATE",
23
+ }
24
+
25
+ CATEGORY_MAP = {
26
+ "allow": "SAFE",
27
+ "flag": "SAFE",
28
+ "remove": "HARASSMENT",
29
+ "escalate": "SAFE",
30
+ }
31
+
32
+
33
+ class ModerationAgent(BaseAgent):
34
+ """
35
+ Content moderation agent using OpenAI API via OpenRouter.
36
+
37
+ Connects to OpenEnv content_moderation_env servers and makes
38
+ moderation decisions (ALLOW, FLAG, REMOVE, ESCALATE).
39
+ """
40
+
41
+ def __init__(self, config: Optional[AgentConfig] = None):
42
+ super().__init__(config or AgentConfig(name="ModerationAgent"))
43
+ self.api_key = os.getenv("HF_TOKEN") or os.getenv("OPENROUTER_API_KEY")
44
+ self.use_api = bool(self.api_key)
45
+
46
+ if self.use_api:
47
+ self.client = OpenAI(
48
+ api_key=self.api_key,
49
+ base_url="https://openrouter.ai/api/v1"
50
+ )
51
+ else:
52
+ logger.warning("No API key found. Using fallback mode.")
53
+ self.client = None
54
+
55
+ def predict(self, observation: Dict[str, Any]) -> Dict[str, Any]:
56
+ """
57
+ Predict a moderation action from an observation.
58
+
59
+ Args:
60
+ observation: Observation dict with post_body, metadata, context
61
+
62
+ Returns:
63
+ Action dict with decision, content_category, reasoning, confidence_score
64
+ """
65
+ if self.use_api:
66
+ try:
67
+ return self._call_api(observation)
68
+ except (APIError, RateLimitError) as e:
69
+ logger.warning(f"API failed: {e}. Using fallback.")
70
+ return self._fallback_decision(observation)
71
+ else:
72
+ return self._fallback_decision(observation)
73
+
74
+ def _call_api(self, obs: Dict[str, Any]) -> Dict[str, Any]:
75
+ """Call OpenAI API for moderation decision."""
76
+ post_body = obs.get("post_body", "")
77
+ metadata = obs.get("metadata", {})
78
+ context = obs.get("context", [])
79
+
80
+ user_prompt = self._build_prompt(post_body, metadata, context)
81
+
82
+ response = self.client.chat.completions.create(
83
+ model="openai/gpt-4.1",
84
+ messages=[
85
+ {
86
+ "role": "system",
87
+ "content": self._get_system_prompt()
88
+ },
89
+ {"role": "user", "content": user_prompt}
90
+ ],
91
+ temperature=self.config.temperature,
92
+ max_tokens=self.config.max_tokens,
93
+ timeout=self.config.timeout
94
+ )
95
+
96
+ content = response.choices[0].message.content.strip().lower()
97
+
98
+ # Parse response
99
+ decision_found = None
100
+ for decision in ["allow", "flag", "remove", "escalate"]:
101
+ if decision in content:
102
+ decision_found = decision
103
+ break
104
+
105
+ if not decision_found:
106
+ decision_found = "escalate"
107
+ logger.warning(f"Unexpected API response: {content}")
108
+
109
+ decision = DECISION_MAP.get(decision_found, "ESCALATE")
110
+ category = CATEGORY_MAP.get(decision_found, "SAFE")
111
+
112
+ return {
113
+ "decision": decision,
114
+ "content_category": category,
115
+ "reasoning": f"API decision: {decision_found}",
116
+ "confidence_score": 0.8
117
+ }
118
+
119
+ def _get_system_prompt(self) -> str:
120
+ return """You are a content moderation agent. Analyze the post and decide:
121
+ - ALLOW: Safe content
122
+ - FLAG: Suspicious content needing review
123
+ - REMOVE: Harmful content
124
+ - ESCALATE: Complex cases requiring human review
125
+
126
+ Respond with only ONE decision word."""
127
+
128
+ def _build_prompt(
129
+ self,
130
+ post_body: str,
131
+ metadata: Dict[str, Any],
132
+ context: list
133
+ ) -> str:
134
+ lines = [
135
+ f"Post: {post_body}",
136
+ f"Author Trust: {metadata.get('author_trust_score', 'N/A')}",
137
+ f"Account Age: {metadata.get('account_age_days', 0)} days",
138
+ f"Reports: {metadata.get('reports_count', 0)}",
139
+ f"Virality: {metadata.get('virality_score', 0)}",
140
+ f"Context: {', '.join(context) if context else 'None'}",
141
+ "",
142
+ "Decide: allow, flag, remove, or escalate?"
143
+ ]
144
+ return "\n".join(lines)
145
+
146
+ def _fallback_decision(self, obs: Dict[str, Any]) -> Dict[str, Any]:
147
+ """
148
+ Keyword-based fallback when API is unavailable.
149
+ """
150
+ post_body = obs.get("post_body", "").lower()
151
+ metadata = obs.get("metadata", {})
152
+ harmful_keywords = {
153
+ "remove": ["kill", "bomb", "explode", "shoot", "murder"],
154
+ "flag": ["hate", "stupid", "dumb", "idiotic", "scum"],
155
+ "spam": ["click here", "earn money", "buy now", "http://", "https://"]
156
+ }
157
+
158
+ for keyword in harmful_keywords["remove"]:
159
+ if keyword in post_body:
160
+ return {
161
+ "decision": "REMOVE",
162
+ "content_category": "HARASSMENT",
163
+ "reasoning": f"Fallback: detected '{keyword}'",
164
+ "confidence_score": 0.6
165
+ }
166
+
167
+ for keyword in harmful_keywords["flag"]:
168
+ if keyword in post_body:
169
+ return {
170
+ "decision": "FLAG",
171
+ "content_category": "HATE_SPEECH",
172
+ "reasoning": f"Fallback: detected '{keyword}'",
173
+ "confidence_score": 0.5
174
+ }
175
+
176
+ # Check trust and reports
177
+ trust = metadata.get("author_trust_score", 0.5)
178
+ reports = metadata.get("reports_count", 0)
179
+ if trust < 0.3 and reports > 5:
180
+ return {
181
+ "decision": "FLAG",
182
+ "content_category": "SAFE",
183
+ "reasoning": "Fallback: low-trust user with many reports",
184
+ "confidence_score": 0.4
185
+ }
186
+
187
+ return {
188
+ "decision": "ALLOW",
189
+ "content_category": "SAFE",
190
+ "reasoning": "Fallback: no harmful patterns",
191
+ "confidence_score": 0.8
192
+ }
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: openenv-agent
3
+ Version: 0.1.0
4
+ Summary: Agent client for OpenEnv environments - connects RL agents to OpenEnv servers for inference and training
5
+ Author: Croma
6
+ License: MIT
7
+ Keywords: openenv,reinforcement-learning,agent,rl,content-moderation
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: openai>=1.3.0
19
+ Requires-Dist: httpx>=0.25.0
20
+ Requires-Dist: pyyaml>=6.0
21
+ Requires-Dist: click>=8.0.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0; extra == "dev"
24
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
25
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # OpenEnv Agent
29
+
30
+ Python client library for connecting RL agents to OpenEnv servers.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install openenv-agent
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ ```python
41
+ from openenv_agent import OpenEnvClient, ModerationAgent
42
+
43
+ # Connect to an OpenEnv server
44
+ client = OpenEnvClient(base_url="http://localhost:8000")
45
+
46
+ # Reset environment
47
+ obs = client.reset()
48
+
49
+ # Use the moderation agent
50
+ agent = ModerationAgent()
51
+ action = agent.predict(obs)
52
+
53
+ # Step the environment
54
+ next_obs, reward, done, info = client.step(action)
55
+ ```
56
+
57
+ ## CLI Usage
58
+
59
+ ```bash
60
+ # Run agent against a server
61
+ openenv-agent run http://localhost:8000
62
+
63
+ # Interactive mode
64
+ openenv-agent interactive http://localhost:8000
65
+
66
+ # Evaluate on a dataset
67
+ openenv-agent eval http://localhost:8000 --dataset ./data.json
68
+ ```
69
+
70
+ ## Features
71
+
72
+ - Async/sync OpenEnv client with Gymnasium-style API
73
+ - Built-in ModerationAgent for content moderation environments
74
+ - Environment loader from openenv.yaml configs
75
+ - CLI tool for easy server interaction
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/openenv_agent/__init__.py
5
+ src/openenv_agent/agent.py
6
+ src/openenv_agent/cli.py
7
+ src/openenv_agent/client.py
8
+ src/openenv_agent/env_loader.py
9
+ src/openenv_agent/moderation_agent.py
10
+ src/openenv_agent.egg-info/PKG-INFO
11
+ src/openenv_agent.egg-info/SOURCES.txt
12
+ src/openenv_agent.egg-info/dependency_links.txt
13
+ src/openenv_agent.egg-info/entry_points.txt
14
+ src/openenv_agent.egg-info/requires.txt
15
+ src/openenv_agent.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ openenv-agent = openenv_agent.cli:main
@@ -0,0 +1,9 @@
1
+ openai>=1.3.0
2
+ httpx>=0.25.0
3
+ pyyaml>=6.0
4
+ click>=8.0.0
5
+
6
+ [dev]
7
+ pytest>=7.0
8
+ pytest-asyncio>=0.21.0
9
+ ruff>=0.1.0
@@ -0,0 +1 @@
1
+ openenv_agent