Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added .secrets.tmp
Empty file.
66 changes: 66 additions & 0 deletions HANDOVER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Railway Use Case — Deployment Handover

## What was built
A Flatland railway dispatcher training tool integrated into InteractiveAI (SystemX).
Two scenarios (Kreuzungskonflikt, Fahrt auf Sichtweite) with Recommendation and Co-Learning modes.

## New services (not in original docker-compose)

### 1. Flask Railway Brain (`railway-brain`)
- **Source:** `usecases_examples/Railway/`
- **Dockerfile:** `usecases_examples/Railway/Railway.Dockerfile`
- **Port:** 5001
- **Runs:** Flatland simulation + scenario player + card publishing

### 2. ZWL Angular Frontend (`zwl-frontend`)
- **Source:** `flatland-hmi-hack4rail/frontend/`
- **Dockerfile:** `flatland-hmi-hack4rail/zwl.Dockerfile`
- **Port:** 4200
- **Shows:** Kartenansicht (map) + ZWL Diagramm (Marey)

## To deploy on server

1. Copy `.env.example` → `.env` and fill in:
```
VITE_RAILWAY_SIMU=http://<SERVER_PUBLIC_IP>:5001
RL_AGENT_API_URL=http://railway-brain:5001/recommendations
```

2. Run:
```bash
docker compose \
-f config/dev/cab-standalone/docker-compose.yml \
-f config/dev/cab-standalone/docker-compose-railway.yml \
up --build
```

3. On first start, run the MongoDB perimeter init manually (timing issue with auto-init):
```bash
docker exec cab-standalone-mongodb-1 mongo operator-fabric \
-u root -p password --authenticationDatabase admin \
/docker-entrypoint-initdb.d/01-cabprocess.js
```

## Decisions for SystemX developer

- **Port 5001 public access:** Flask brain must be reachable from the browser.
Options: expose port directly, or proxy via nginx at `/railway-api/`.
If proxied, update all `BACKEND_URL` in the Angular services + Vue frontend.

- **Authentication:** `AUTH_DISABLED=true` is set everywhere.
Enable auth when deploying to production.

- **ZWL build output path:** Check `angular.json` `outputPath` — the Dockerfile
assumes `dist/frontend/browser`. Adjust if different.

- **Maps volume:** `usecases_examples/Railway/maps/` contains JSON map files
needed at runtime. Mount as volume or bake into Docker image.

## Files changed from original InteractiveAI repo
- `frontend/src/entities/Railway/CAB/` — all CAB Vue components
- `usecases_examples/Railway/app.py` — Flask brain (heavily extended)
- `usecases_examples/Railway/ScenarioPlayer.py` — new file
- `usecases_examples/Railway/FlatlandMapLoader.py` — new file
- `usecases_examples/Railway/ExperimentLogger.py` — new file
- `experiment_scenarios/` — new directory with scenario definitions
- `flatland-hmi-hack4rail/frontend/src/app/` — ZWL Angular components
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,85 @@ Contributions to the InteractiveAI Assistant Platform are welcome! To contribute
# Docs
A postman collection is under docs/postman_collections.
You can also check the openapi through the URL http://localhost:[Service port]/docs


---

## Railway Use Case (FHNW / AI4REALNET)

The Railway use case adds a Flatland train simulation with interactive scenario-based dispatcher training. It requires **two additional services** beyond the main Docker stack.

> See also `HANDOVER.md` for deployment decisions.

### Additional Prerequisites

- **Python 3.10** (exact version required for `flatland-rl`)
- **Node.js 18+**

### Step-by-step Local Setup

After completing the standard InteractiveAI setup above, add `VITE_RAILWAY_SIMU=http://localhost:5001` to your `.secrets` file, then:

**1. Install Python dependencies (first time only)**
```bash
cd usecases_examples/Railway
python3.10 -m venv .venv
source .venv/bin/activate # Linux/Mac
# .venv\Scripts\activate # Windows
pip install -r requirements.txt
cd ../..
```

**2. Start Flask Railway brain** *(new terminal)*
```bash
cd usecases_examples/Railway
source .venv/bin/activate
python app.py
```
Ready when: `Running on http://0.0.0.0:5001`

**3. Start ZWL Angular frontend** *(new terminal)*
```bash
cd flatland-hmi-hack4rail/frontend
npm install # first time only
npm start
```
Ready when: `Local: http://localhost:4200`

**4. MongoDB perimeter setup** *(required after every Docker restart)*
```bash
docker exec cab-standalone-mongodb-1 mongo operator-fabric \
-u root -p password --authenticationDatabase admin \
--eval 'db.perimeter.updateOne({_id:"cabProcess"},{$set:{process:"cabProcess",stateRights:[{state:"messageState",right:"ReceiveAndWrite"}]}},{upsert:true}); db.group.updateOne({_id:"Planner"},{$addToSet:{perimeters:"cabProcess"}}); db.group.updateOne({_id:"Dispatcher"},{$addToSet:{perimeters:"cabProcess"}}); print("done")'
```

Open `http://localhost:3200/cab/Railway` and log in as `railway_user` / `test`.

### Scenarios

| ID | Name | Description |
|----|------|-------------|
| `scenario1` | Kreuzungskonflikt | Single-track crossing conflict |
| `scenario2` | Fahrt auf Sichtweite | Speed restriction causing dispatch conflict |
| `scenario3` | Zugreihenfolge | Multiple delays disrupting train order |

### Server Deployment (Docker)

Dockerfiles are provided to containerise Flask and the ZWL frontend:

```bash
docker compose \
-f config/dev/cab-standalone/docker-compose.yml \
-f config/dev/cab-standalone/docker-compose-railway.yml \
up --build
```

Set `VITE_RAILWAY_SIMU=http://<SERVER_PUBLIC_IP>:5001` in `.secrets` before building. See `HANDOVER.md` for open deployment decisions (port exposure, auth, nginx proxy).

### Troubleshooting

| Problem | Solution |
|---------|----------|
| Kartenansicht: "cannot connect to localhost:4200" | ZWL Angular not running — run step 3 |
| No train data | Flask not running — run step 2 |
| No notification cards | Re-run step 4 (MongoDB command) |
112 changes: 39 additions & 73 deletions backend/recommendation-service/resources/Railway/manager.py
Original file line number Diff line number Diff line change
@@ -1,85 +1,51 @@
# backend/recommendation-service/resources/Railway/manager.py
import json
from api.manager.base_manager import BaseRecommendationManager
from .mockRecommendations.mockRecommendations import RECOMMENDATION_CATALOG
from .sncf_recommender import SNCF_RECO3, SNCF_deontic, SNCF_risk, SNCF_risk_tie_break

import logging

logger = logging.getLogger(__name__)
import os
import requests
from api.manager.base_manager import BaseRecommendationManager
from settings import logger


class RailwayManager(BaseRecommendationManager):
def __init__(self):
# URL of our Flask brain's /recommendations endpoint
# Set RL_AGENT_API_URL in .env to point at the Flask brain
self.agent_api_url = os.environ.get(
"RL_AGENT_API_URL",
"http://host.docker.internal:5001/recommendations",
)
self.agent_api_token = os.environ.get("RL_AGENT_API_TOKEN", "")
super().__init__()

def _transform_recommendation(self, reco_json):
"""Transform a recommendation from catalog format to API output format."""
reco = json.loads(reco_json)
return {
"title": reco["data"]["title"],
"description": reco["data"]["description"],
"use_case": reco["data"]["use_case"],
"agent_type": reco["data"]["agent_type"],
"actions": [{}],
"kpis": reco["data"]["kpis"],
}

def get_recommendation(self, request_data):
"""
Return recommendations for a Railway event.

Supports four modes controlled by request_data["event"]["mode"]:

- "basic" (default): best-first ordering via SNCF_RECO3
- "deontic": filter by KPI threshold, then sort ascending via SNCF_deontic
requires: sort_type ("passengers"|"delay"|"cost"|"total_cost")
threshold_value (int, or delay string e.g. "1h30" for delay)
- "risk": sort all recommendations by KPI ascending via SNCF_risk
requires: sort_type
- "risk_tie_break": sort by primary KPI, break ties with secondary via SNCF_risk_tie_break
requires: sort_type
optional: tie_breaker (same values as sort_type)
Calls our Flask brain's /recommendations endpoint and returns
the result in the format InteractiveAI expects.
"""
event_data = request_data.get("event", {})
context_data = request_data.get("context", {})

# Ensure id_event has a fallback so catalog lookup always has a key
event_for_sncf = {**event_data, "id_event": str(event_data.get("id_event", "1"))}

# Wrap into the structure expected by SNCF functions
event_json = json.dumps({"data": event_for_sncf})
context_json = json.dumps({"data": context_data})

mode = event_data.get("mode", "deontic")
logger.info(f"Railway recommendation — event_id: {event_for_sncf['id_event']}, mode: {mode}")

if mode == "deontic":
sort_type = event_data.get("sort_type", "cost")
threshold_value = event_data.get("threshold_value")
recommendations = SNCF_deontic(
event_json, context_json, RECOMMENDATION_CATALOG,
type=sort_type, threshold_value=threshold_value,
)
elif mode == "risk":
sort_type = event_data.get("sort_type", "cost")
recommendations = SNCF_risk(
event_json, context_json, RECOMMENDATION_CATALOG,
type=sort_type,
headers = {"Content-Type": "application/json"}
if self.agent_api_token:
headers["Authorization"] = "Bearer " + self.agent_api_token

try:
response = requests.post(
self.agent_api_url,
json=request_data,
headers=headers,
timeout=10,
verify=False,
)
elif mode == "risk_tie_break":
sort_type = event_data.get("sort_type", "cost")
tie_breaker = event_data.get("tie_breaker")
recommendations = SNCF_risk_tie_break(
event_json, context_json, RECOMMENDATION_CATALOG,
type=sort_type, tie_breaker=tie_breaker,
)
else: # "basic" or any unrecognised mode
recommendations = SNCF_RECO3(event_json, context_json, RECOMMENDATION_CATALOG)

# Surface catalog errors as an empty list rather than crashing downstream
if recommendations and isinstance(recommendations[0], dict) and "error" in recommendations[0]:
logger.error(f"Recommendation error: {recommendations[0]['error']}")
return []

return [self._transform_recommendation(reco) for reco in recommendations]
recommendations = response.json()
logger.info("Railway recommendations received: " + str(len(recommendations)))
return recommendations

except Exception as e:
logger.error("Failed to get Railway recommendations: " + str(e))
# Return a fallback so the UI doesn't break
return [{
"title": "No recommendations available",
"description": "Could not reach the simulation brain. Please try again.",
"use_case": "Railway",
"agent_type": "AI",
"actions": [{}],
"kpis": {},
}]
51 changes: 51 additions & 0 deletions config/dev/cab-standalone/docker-compose-railway.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# docker-compose override — Railway use case services
# Usage: docker compose -f config/dev/cab-standalone/docker-compose.yml \
# -f config/dev/cab-standalone/docker-compose-railway.yml up

version: '3.5'
services:

# Flask Railway brain (app.py)
railway-brain:
container_name: railway-brain
build:
context: ../../../usecases_examples/Railway
dockerfile: Railway.Dockerfile
restart: unless-stopped
ports:
- '5001:5001' # exposed so browser can reach it directly
volumes:
- railway_logs:/app/experiment_logs
- railway_maps:/app/maps
environment:
- PYTHONUNBUFFERED=1
# Cards-publication reachable via Docker network
depends_on:
- cards-publication

# ZWL Angular frontend (Kartenansicht + Marey diagram)
zwl-frontend:
container_name: zwl-frontend
build:
context: ../../../flatland-hmi-hack4rail
dockerfile: zwl.Dockerfile
args:
# Must be reachable FROM THE BROWSER — use server public IP/hostname
# Local dev: http://localhost:5001
# Deployment: http://<SERVER_IP>:5001
RAILWAY_SIMU_URL: ${VITE_RAILWAY_SIMU:-http://localhost:5001}
restart: unless-stopped
ports:
- '4200:80' # same port as local npm start

# MongoDB with auto-perimeter init for cabProcess cards
mongodb:
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: password
volumes:
- ../../../config/dev/cab-standalone/mongo-init.js:/docker-entrypoint-initdb.d/01-cabprocess.js:ro

volumes:
railway_logs:
railway_maps:
30 changes: 30 additions & 0 deletions config/dev/cab-standalone/mongo-init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// MongoDB init script — runs once when the container is first created
// Sets up the cabProcess perimeter so Railway notification cards work

db = db.getSiblingDB('operator-fabric');

db.perimeter.updateOne(
{ _id: 'cabProcess' },
{ $set: {
process: 'cabProcess',
stateRights: [{ state: 'messageState', right: 'ReceiveAndWrite' }]
}},
{ upsert: true }
);

db.group.updateOne(
{ _id: 'Planner' },
{ $addToSet: { perimeters: 'cabProcess' } }
);

db.group.updateOne(
{ _id: 'Dispatcher' },
{ $addToSet: { perimeters: 'cabProcess' } }
);

db.group.updateOne(
{ _id: 'ReadOnly' },
{ $addToSet: { perimeters: 'cabProcess' } }
);

print('cabProcess perimeter initialized');
3 changes: 3 additions & 0 deletions flatland-hmi-hack4rail/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

# node_modules directory
node_modules
20 changes: 20 additions & 0 deletions flatland-hmi-hack4rail/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"braceStyle": "1tbs",
"bracketSameLine": false,
"bracketSpacing": true,
"phpVersion": "8.1",
"printWidth": 120,
"proseWrap": "preserve",
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
}
]
}
Loading
Loading