Metadata-Version: 2.4
Name: nornos-gym
Version: 0.2.0
Summary: Gym-compatible wrapper for the NornOS Hosted Research API (research.laif2.com).
Home-page: https://github.com/wmaass/nornos_v3/tree/master/research/python
Project-URL: Documentation, https://github.com/wmaass/nornos_v3/blob/master/research/python/README.md
Project-URL: Source, https://github.com/wmaass/nornos_v3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: License :: Other/Proprietary License
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Operating System :: OS Independent
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: gymnasium>=0.29
Requires-Dist: numpy>=1.24
Requires-Dist: requests>=2.28
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: project-url
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# nornos-gym

Gym-compatible wrapper for the NornOS Hosted Research API. Pure Python +
gymnasium + numpy + requests. No NornOS-internal knowledge required to use.

## Status

`v0.2.0` — Hosted API + Bearer-Token-Auth + 429-Retry. Skill outputs not yet
integrated into reward (fallback uses feature-deviation-from-mean, see
PRD-NORNOS-CORE-001 §5.4).

## Quickstart (Hosted API — researcher path)

```bash
pip install nornos-gym
```

Request an API key from the operator (see `docs/onboarding.md`), then:

```python
from nornos_gym import NornOSEnv

env = NornOSEnv(
    base_url="https://research.laif2.com",
    api_key="nornos-research-<your-uuid>",
)
obs, info = env.reset()
for _ in range(100):
    action = env.action_space.sample()
    obs, reward, terminated, truncated, info = env.step(action)
    print(f"reward={reward:.3f} audit={info['auditHash'][:8]}")
env.close()
```

The client automatically retries with exponential backoff (max 5 attempts)
when the server returns 429 Too Many Requests, respects `Retry-After` headers,
and applies ±25% jitter to spread retries.

### Error handling

```python
from nornos_gym import NornOSEnv, NornOSAuthError, RateLimitError

try:
    env = NornOSEnv(base_url="https://research.laif2.com", api_key="...")
    obs, info = env.reset()
except NornOSAuthError as e:
    print(f"Token issue: {e}")  # 401 missing, 403 invalid/revoked
except RateLimitError as e:
    print(f"Rate limit persistent after retries: {e}")
```

## Local stack (development)

```bash
# 1. Clone repo, then:
docker compose -f docker-compose.research.yml up -d

# 2. Install editable:
pip install -e research/python/

# 3. Run without api_key (local stack has no auth):
python -c "
from nornos_gym import NornOSEnv
env = NornOSEnv(base_url='http://localhost:4400')
obs, info = env.reset()
print(f'entities: {len(info[\"auditHashes\"])}')
env.close()
"
```

## Public API

- `NornOSEnv(base_url, seed_id, entity_filter)` — Gym 0.26+ environment
- `NornOSMultiAgentEnv(base_url, seed_id, agent_entity_map)` — MARL via ThreadPoolExecutor
- `NornOSAdapter` — base class for domain-specific feature extraction + reward shaping

## Adapter Quickstart

To plug a domain into NornOS, subclass `NornOSAdapter`:

```python
from nornos_gym import NornOSAdapter

class CircularEconomyAdapter(NornOSAdapter):
    def __init__(self):
        super().__init__(feature_keys=["material_weight", "recyclability_score", "co2_footprint"])

    def compute_reward(self, skill_outputs):
        f = skill_outputs.get("features", skill_outputs)
        return (1 - float(f.get("recyclability_score", 0.0))) * \
               float(f.get("co2_footprint", 0.0))
```

5-Step recipe:

1. Define your domain features as a fixed `feature_keys` list
2. Write a custom seed JSON under `research/<your-domain>/<name>_seed.json` with `entityId` / `entityType` / `features` per entity
3. Add a `COPY` line to `docker/compose/Dockerfile.event-log-service-research` that maps your seed into `/app/research/seeds/<your-seed-id>.json`
4. Subclass `NornOSAdapter`, override `compute_reward` (and optionally `extract_features`) with your domain logic
5. Use the adapter in your training loop alongside `NornOSEnv` — see `research/adapter-example/run_episode.py`

Full spec, anti-patterns, testing template: **`research/docs/adapter-interface.md`**.

## Run the circular-economy example

```bash
docker compose -f docker-compose.research.yml up -d
python research/adapter-example/run_episode.py --seed-id circular-economy-default --steps 100
```

Output: server-reward (deviation fallback) and adapter-reward (domain
logic) side by side, plus top-3 most-problematic products by mean
adapter score.

## Building a custom adapter

Adapters are pure Python — no NornOS internals, no library touch. Three
hard rules from `adapter-interface.md` §4:

- **Bound your rewards.** PPO/DQN assume a stable range; clip to `[0, 1]` or `[-1, 1]`.
- **Stay stateless.** Don't hold per-step state on `self`; the adapter is reused across episodes.
- **Public API only.** `from nornos_gym import NornOSAdapter` is the only NornOS import allowed. No `apps/maritime`, no `services/event-log-service`, no internal modules.

## Disclaimer

Research-only distribution. Not for clinical, safety-critical, or production
use. The Compose stack ships with hardcoded `-insecure` tokens that must
never appear in production deployments.
