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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Sonarqube
.scannerwork

# Externally cloned service (T2.1 deep expert agent), not part of this repo
/T2.1_deep_expert/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ _Backend_
<li>
<a href="#getting-started">Getting Started</a>
<ul>
<li><a href="#quick-start-local-install-on-a-single-machine">Quick Start: Local Install on a Single Machine</a></li>
<li><a href="#prerequisites">Prerequisites</a></li>
<li><a href="#setting-up-the-environment">Setting Up the Environment</a></li>
</ul>
Expand All @@ -46,6 +47,20 @@ The platform uses the project **OperatorFabric** for notification management.
<!-- GETTING STARTED -->
## Getting Started

### Quick Start: Local Install on a Single Machine

If you just want everything running on one machine, you don't need to follow the
manual steps below — the repo ships two helper scripts that do it all:

```sh
./local_setup.sh # start the whole stack (backend, frontend, Keycloak, PowerGrid simulator)
./local_stop.sh # tear it down again
```

`./local_setup.sh --help` and `./local_stop.sh --help` list the available options
(e.g. `--clean` / `--wipe` for a fresh start, `--pause` to keep containers around).
The rest of this section describes the manual/multi-machine setup.

### Prerequisites

- [Git (version 2.40.1)](https://git-scm.com/)
Expand Down Expand Up @@ -155,6 +170,34 @@ Some examples of credentials:

By default, the system allows the user to be connected only from a single machine. Which means if you try to connect using the same credentials from another machine, you will be disconnected on the first machine.

### OPTIONAL: connecting a local RL agent (T2.1 deep-expert)

By default, PowerGrid recommendations use the external RL agent API. To run and
connect a local agent instead:

```bash
git clone -b docker https://github.com/ainetus/T2.1_deep_expert.git
cd T2.1_deep_expert
docker build -t t2-1-deep-expert .
docker run -d --name t2-1-deep-expert -p 8000:8000 t2-1-deep-expert
```

Then point `cabrecommendation` at it:
1. In `config/dev/cab-standalone/docker-compose.yml`, add to the `cabrecommendation` service:
```yaml
extra_hosts:
- "host.docker.internal:host-gateway"
```
2. In `config/dev/cab-standalone/.secrets` (copy from `.secrets.example` if needed), set:
```bash
export RL_AGENT_API_URL=http://host.docker.internal:8000/api/v1/recommendation
```
3. Re-run `./docker-compose.sh` from that directory to regenerate `.env` and recreate `cabrecommendation`.

To disconnect, remove the `extra_hosts` block and `.secrets` (or its
`RL_AGENT_API_URL` line) and re-run `./docker-compose.sh` again — this restores
the default external RL agent URL.

# Development

Contributions to the InteractiveAI Assistant Platform are welcome! To contribute, please make sure to use [developer guide](docs/developer-guide.md)
Expand Down
20 changes: 11 additions & 9 deletions backend/Recommendation-Service-Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ RUN apt-get update && apt-get install -y default-jre
RUN mkdir /my_app
RUN mkdir /cab_common

# Install recommendation-service modules
COPY ./recommendation-service/ /my_app/
COPY ./cab_common/ /cab_common/

# Install the (large: torch/tensorflow/grid2op/lightsim2grid) Python deps
# before copying the rest of the source, so this slow layer is cached and
# only rebuilt when requirements.txt actually changes, not on every source
# edit. The pip cache mount persists downloaded wheels across builds too,
# so even a real requirements.txt change or a retry after a network blip
# doesn't have to re-download unchanged packages.
WORKDIR /my_app

RUN pip3 install -r requirements.txt
COPY ./recommendation-service/requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip pip3 install -r requirements.txt

WORKDIR /cab_common

RUN pip3 install .

COPY ./cab_common/ .
RUN --mount=type=cache,target=/root/.cache/pip pip3 install .

WORKDIR /my_app
COPY ./recommendation-service/ .

CMD ["./entrypoint.sh"]
Binary file modified backend/recommendation-service/requirements.txt
Binary file not shown.
10 changes: 8 additions & 2 deletions backend/recommendation-service/resources/PowerGrid/manager.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import json
import os
import warnings
from enum import Enum

import requests
import urllib3
from api.manager.base_manager import BaseRecommendationManager
from owlready2 import get_ontology
from settings import logger

from .PowerGridgrid2op_poc_simulator.assistant_manager import AgentType

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class AgentType(Enum):
"""Recommendations' agent type: "onto" (ontology) or "IA" (RL agent)."""

onto = 1
IA = 2


class PowerGridManager(BaseRecommendationManager):
"""PowerGrid recomendation service

Expand Down
56 changes: 56 additions & 0 deletions backend/recommendation-service/tests/test_smoke_pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""End-to-end smoke test of the PowerGrid recommendation pipeline.

There is no local RL agent service in this repo — PowerGrid recommendations
combine an *external* RL agent API (best-effort, network-dependent) with the
ontology recommender that ships in this service. This test isolates the part
that must always work offline: post a context/event exactly as the simulator
serializes it, go through the real Flask view + auth + use-case dispatch, and
get back at least one well-formed ontology recommendation. The RL agent call
is stubbed out so the test doesn't depend on network access to the external
service.
"""
import json

POWERGRID_BEARER_TOKEN = "dummy-token-see-PowerGrid_auth_mocker-fixture"


def test_pipeline_smoke_context_to_recommendation(
client, create_usecases, PowerGrid_auth_mocker, mocker
):
"""Post a simulator-shaped context and get an actionable ontology recommendation back."""
# No local/reachable RL agent in this environment — stub it out so the
# pipeline is exercised deterministically, offline, end to end.
mocker.patch(
"resources.PowerGrid.manager.PowerGridManager._get_rl_parades",
return_value=[],
)

with open("tests/tests_resources/rte_recommendation.json") as json_file:
payload = json.load(json_file)

headers = {"Authorization": f"Bearer {POWERGRID_BEARER_TOKEN}"}
response = client.post(
"/api/v1/recommendation?use_case=PowerGrid",
headers=headers,
json=payload,
)

assert response.status_code == 200
recommendations = response.get_json()
assert isinstance(recommendations, list) and len(recommendations) >= 1

# Every recommendation is well-formed and tagged with its source.
for reco in recommendations:
assert reco["use_case"] == "PowerGrid"
assert reco["agent_type"] in {"IA", "onto"}
assert reco["title"]
assert "kpis" in reco

# With the RL agent stubbed out, only the ontology recommender can have
# answered — confirm the pipeline actually produced one (not just the
# "no recommendation found" default), with an efficiency KPI attached.
onto_recos = [r for r in recommendations if r["agent_type"] == "onto"]
assert onto_recos
assert any(
"efficiency_of_the_reco" in (r["kpis"] or {}) for r in onto_recos
)
2 changes: 1 addition & 1 deletion backend/recommendation-service/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
def test_PowerGrid_get_recommendation(client, create_usecases, PowerGrid_auth_mocker):
recommendation_data = {}
# Opening JSON file
with open("tests/tests_resources/PowerGrid_recommendation.json") as json_file:
with open("tests/tests_resources/rte_recommendation.json") as json_file:
recommendation_data = json.load(json_file)
headers = {"Authorization": f"Bearer {POWERGRID_BEARER_TOKEN}"}
response = client.post(
Expand Down
158 changes: 158 additions & 0 deletions changelog/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Changelog

Bug fixes and infrastructure/feature changes made to this repo, in the order
they were done. Each entry says what was broken, how it was found/verified,
and what changed.

---

## 1. Added `local_setup.sh` / `local_stop.sh`

One-shot scripts to bring the local stack up and down: start the
`cab-standalone` backend, wait for Keycloak and configure it via the admin
REST API, rebuild `frontend`/`cabrecommendation` from source, load
OperatorFabric resources and register use cases, and build/start the
PowerGrid simulator. `local_stop.sh` supports `--pause` (stop, keep containers),
default (remove containers, keep data volumes), and `--wipe` (remove
volumes too). Verified with a full live `docker compose` bring-up and both
teardown modes.

## 2. Port conflict: `cabcontext` vs. PowerGrid simulator (both `5100`)

`config/dev/cab-standalone/docker-compose.yml` published `cabcontext` on host
port `5100`, colliding with the PowerGrid simulator app (also `5100`).
Remapped `cabcontext` to `5101:5000`.

## 3. Hardcoded personal LAN IP in the PowerGrid simulator proxy

`nginx-cors-permissive.conf`'s `/powergrid-simu/` location proxied to
`http://192.168.208.61:5100/` — a machine-specific address that only worked
on the machine it was written on. Replaced with `http://host.docker.internal:5100/`,
and added `extra_hosts: ["host.docker.internal:host-gateway"]` to the
`frontend` service (cab-standalone) and to the PowerGrid simulator's `app`
service, so the mapping resolves on any machine. Also added
`cab_server_url_0 = "http://host.docker.internal:3200/"` to
`API_POWERGRID_CAB.toml` for a working local-dev server option in the
simulator's login page.

## 4. Entity assignment to `publisher_test` missing from `resources/loadTestConf.sh`

Without it, `publisher_test` had no entities assigned and the Home page
showed no entity cards, making PowerGrid/ATM/Railway unreachable. Added a
`PUT /users/users/publisher_test` call (assigning `PowerGrid`, `ATM`,
`Railway`) after use cases are registered.

## 5. `usecases_examples/PowerGrid/Dockerfile.app` was cache-unfriendly and bloated

Code was copied before `requirements-app.txt` (busts the dependency layer
cache on every source edit), the apt install used no
`--no-install-recommends` and didn't clean `/var/lib/apt/lists`, and a stray
`EXPOSE 5000` remained despite port publishing being handled by compose.
Reordered (deps before source) and cleaned up.

## 6. PowerGrid recommendation-service could not import

`backend/recommendation-service/resources/PowerGrid/manager.py` imported
`AgentType` from a `PowerGridgrid2op_poc_simulator` package that doesn't
exist in this repo. Every PowerGrid use-case registration
(`POST /api/v1/usecases`) raised `ModuleNotFoundError` — the ontology
recommendation pipeline for PowerGrid was dead on arrival. Fixed by inlining
the small `AgentType` enum directly in `manager.py`. Verified live: use-case
registration, `POST /api/v1/recommendation`, and the smoke test (below) all
pass.

## 7. Broken/incomplete test infrastructure in `backend/recommendation-service`

- `tests/test_views.py::test_PowerGrid_get_recommendation` opened
`tests/tests_resources/PowerGrid_recommendation.json`, which doesn't exist —
only `rte_recommendation.json` (same shape/content) is present. Fixed the
filename.
- `pytest-mock` was missing from `requirements.txt` despite `conftest.py`'s
`PowerGrid_auth_mocker` fixture already depending on the `mocker` fixture —
the existing test suite could not have run as committed. Added
`pytest-mock==3.14.0`.
- Added `test_smoke_pipeline.py`: an end-to-end smoke test of the PowerGrid
recommendation pipeline. It stubs out the external RL call (no local agent
to exercise by default) and asserts the ontology-recommendation path
returns a well-formed recommendation through the real Flask view/auth
stack. All 3 tests in the service pass (verified inside the actual
`cab_recommendation` container, which has the full dependency stack —
running `pytest` bare on a host machine without `cab_common` installed and
the right `PYTHONPATH`/cwd will not work).

## 8. Frontend: auto-logout on a dead session token was disabled ("TEMP HACK (eval-demo)")

Reported symptom: repeated `Unauthorized` on `/cabcontext/api/v1/contexts`
with no recovery. Root cause: `frontend/src/plugins/http.ts` had the recovery
block (call `authStore.checkToken()` on a failed request, log out and
redirect to `/login` if the token is truly dead) fully commented out, marked
`TODO: TEMP HACK (eval-demo) — MUST BE REMOVED before next release`. Once a
token stopped being valid (e.g. idle session timeout), the frontend had no
recovery path and kept resending the same dead token forever. Re-enabled the
block. Rebuilt and confirmed the bundle no longer contains the hack.

## 9. Frontend: `getRecommendation` silently dropped the `use_case` query parameter

Reported symptom: clicking "fetch recommendations" failed with `400 Bad
Request`. Root cause: `frontend/src/api/services.ts`'s `getRecommendation`
didn't accept or send a `use_case` parameter at all, but the
recommendation-service requires one when a token is registered for more than
one entity (e.g. `publisher_test` with `Railway;ATM;PowerGrid`). Fixed by
adding `use_case` as an optional query param on `getRecommendation` in
`api/services.ts`, passed from `stores/services.ts` as
`event.entityRecipients[0]`. Reproduced the exact 400 with a real
multi-entity token and confirmed the fix resolves it (200 with a real
ontology recommendation).

## 10. Frontend: `applyRecommendation` was faking success for ATM/Railway/PowerGrid ("TEMP HACK (eval-demo)")

Three separate copies of the same disabled-with-fake-success pattern:

- `frontend/src/api/services.ts` — dead code, not imported/used anywhere
(each entity has its own `applyRecommendation` in `entities/<Entity>/api.ts`).
Restored the real `http.post('/api/v1/recommendations', data)` call anyway,
for consistency.
- `frontend/src/entities/ATM/api.ts` and `frontend/src/entities/Railway/api.ts`
— these ARE live/used by the UI. Restored their real simulator calls
(`VITE_ATM_SIMU + '/update-flight-plan'`, `VITE_RAILWAY_SIMU + '/transport_plan'`).
Note: this repo has no ATM or Railway simulator (only
`usecases_examples/PowerGrid` exists, and `VITE_ATM_SIMU`/`VITE_RAILWAY_SIMU`
default to `"false"`), so applying a recommendation for those two entities
will now fail with a real network error instead of silently pretending to
succeed — expected until those simulators exist or are pointed at a real
endpoint.

PowerGrid's own `applyRecommendation` (`entities/PowerGrid/api.ts`) was
already live and was not touched.

## 11. PowerGrid simulator login page was in French

`usecases_examples/PowerGrid/app/templates/index.html` had `lang="fr"` and
all UI text (labels, buttons, alerts) in French, while the rest of the app
(flash messages) is in English. Translated the page to English (`lang="en"`,
"Login", "Select a server", "Username", "Password", error alerts, etc).
Verified live at `http://localhost:5100/`.

## 12. Documented how to connect a local RL agent

Added a README section describing how to clone, build, and run the
`T2.1_deep_expert` RL agent service locally, and how to wire it into
`cabrecommendation` (`extra_hosts` + `RL_AGENT_API_URL` in `.secrets`) or
revert back to the default external agent URL. Verified end-to-end with a
live local agent: a PowerGrid recommendation request returned both a real
RL-agent recommendation and the ontology recommendation together. The
default state of this repo is disconnected (external agent URL, no local
`extra_hosts`/`.secrets` override).

**Known follow-up, not yet fixed:** the RL agent returns `agent_type` as a
raw enum int (`2`) rather than the string `"IA"` that the ontology path uses
and that the frontend (`Recommendations.vue`/`Assistant.vue`) checks
against — a recommendation from that agent may not render/behave correctly
in the UI as a result.

## Not investigated / out of scope so far

- The PowerGrid simulator's "line lost" event branch does not call the CAB
recommendation API the way the "overload anticipation" branch does (only
pauses for manual continuation).
- The `agent_type` int-vs-string mismatch noted in item 12.
9 changes: 8 additions & 1 deletion config/dev/cab-standalone/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ services:
restart: unless-stopped
command: sh -c "./entrypoint.sh"
ports:
- 5100:5000
# was 5100:5000, which collides with the PowerGrid simulator app (also
# published on host port 5100) — see usecases_examples/PowerGrid/docker-compose.yml
- 5101:5000
volumes:
- ../../../backend/context-service:/code
depends_on:
Expand Down Expand Up @@ -149,6 +151,11 @@ services:
- './nginx-cors-permissive.conf:/etc/nginx/conf.d/default.conf'
ports:
- '3200:80'
# nginx proxies /powergrid-simu/ to the PowerGrid simulator running on the
# host (see nginx-cors-permissive.conf); host.docker.internal resolves to
# the host gateway on Linux only if mapped explicitly.
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- cabrecommendation
- cabevent
Expand Down
6 changes: 5 additions & 1 deletion config/dev/cab-standalone/nginx-cors-permissive.conf
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,11 @@ server {
location /powergrid-simu/ {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://192.168.208.61:5100/;
# FIX: was a hardcoded machine-specific LAN IP (192.168.208.61), which
# only worked on the machine it was written on. host.docker.internal
# resolves to the host gateway on any machine (mapped via extra_hosts
# on the frontend service in docker-compose.yml).
proxy_pass http://host.docker.internal:5100/;
}

location /cognitive-api/ {
Expand Down
Loading