From bc60b854681de501721b6f73461c8ed496cc3933 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 06:58:40 +0000 Subject: [PATCH] feat(auto): transform repository into an autonomous management system - Implement CodeRabbit AI PR reviews and update Node.js versions in CI. - Split health dashboard deployment into a dedicated workflow. - Implement comprehensive GitHub Actions (CI, CodeQL, Stale, Greetings, Labeler). - Add foundational OSS files (CODE_OF_CONDUCT.md, CONTRIBUTING.md, CODEOWNERS). - Build autonomous Python scripts for docs generation, architecture diagrams, and knowledge graphs. - Establish a daily repo-maintenance workflow to generate SBOMs and automatically commit generated artifacts. - Enforce pre-commit configurations for Ruff and Prettier. Co-authored-by: NITISH-R-G <225521762+NITISH-R-G@users.noreply.github.com> --- .github/CODEOWNERS | 1 + .github/labeler.yml | 29 +++++++ .../{ai-insights.yml => ai-review.yml} | 8 +- .github/workflows/ci.yml | 31 ++++++++ .github/workflows/code-quality.yml | 4 +- .github/workflows/codeql.yml | 40 ++++++++++ .github/workflows/greetings.yml | 20 +++++ .github/workflows/health-dashboard.yml | 7 -- .github/workflows/labeler.yml | 15 ++++ .github/workflows/pages.yml | 46 +++++++++++ .github/workflows/repo-maintenance.yml | 64 +++++++++++++++ .github/workflows/stale.yml | 21 +++++ .gitignore | 8 ++ .pre-commit-config.yaml | 21 +++++ CODE_OF_CONDUCT.md | 49 ++++++++++++ CONTRIBUTING.md | 39 +++++++++ ev_grid_oracle/bescom_feed.py | 2 +- ev_grid_oracle/city_graph.py | 5 +- ev_grid_oracle/env.py | 14 ++-- ev_grid_oracle/grid_sim.py | 2 +- ev_grid_oracle/models.py | 12 ++- ev_grid_oracle/oracle_agent.py | 9 +-- ev_grid_oracle/parsing.py | 10 +-- ev_grid_oracle/personas.py | 1 - ev_grid_oracle/reward.py | 3 +- ev_grid_oracle/road_models.py | 3 +- ev_grid_oracle/scenarios.py | 3 +- ev_grid_oracle/traffic.py | 2 +- ev_grid_oracle/world_model_verifier.py | 2 +- server/app.py | 35 ++++---- server/road_router.py | 11 +-- server/role_metrics.py | 2 +- tools/build_road_graph.py | 16 ++-- tools/build_roads_render.py | 1 - tools/docs_sync.py | 74 +++++++++++++++++ tools/fetch_bangalore_roads_overpass.py | 3 +- tools/fetch_osm_roads.py | 1 - tools/generate_architecture_diagrams.py | 63 +++++++++++++++ tools/generate_health_dashboard.py | 4 +- tools/generate_knowledge_graph.py | 79 +++++++++++++++++++ tools/road_reward_smoke.py | 4 +- training/train_grpo.ipynb | 4 +- viz/city_map.py | 34 ++++---- viz/gradio_demo.py | 3 +- viz/record.py | 3 +- viz/record_two_phase.py | 5 +- 46 files changed, 691 insertions(+), 122 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/labeler.yml rename .github/workflows/{ai-insights.yml => ai-review.yml} (55%) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/greetings.yml create mode 100644 .github/workflows/labeler.yml create mode 100644 .github/workflows/pages.yml create mode 100644 .github/workflows/repo-maintenance.yml create mode 100644 .github/workflows/stale.yml create mode 100644 .pre-commit-config.yaml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 tools/docs_sync.py create mode 100644 tools/generate_architecture_diagrams.py create mode 100644 tools/generate_knowledge_graph.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..c0084b41 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @NITISH-R-G \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000..0cb601d7 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,29 @@ +frontend: + - changed-files: + - any-glob-to-any-file: 'web/**/*' + +backend: + - changed-files: + - any-glob-to-any-file: 'ev_grid_oracle/**/*' + - any-glob-to-any-file: 'server/**/*' + +training: + - changed-files: + - any-glob-to-any-file: 'training/**/*' + +tests: + - changed-files: + - any-glob-to-any-file: 'tests/**/*' + +docs: + - changed-files: + - any-glob-to-any-file: 'docs/**/*' + - any-glob-to-any-file: '*.md' + +tools: + - changed-files: + - any-glob-to-any-file: 'tools/**/*' + +github-actions: + - changed-files: + - any-glob-to-any-file: '.github/**/*' \ No newline at end of file diff --git a/.github/workflows/ai-insights.yml b/.github/workflows/ai-review.yml similarity index 55% rename from .github/workflows/ai-insights.yml rename to .github/workflows/ai-review.yml index 87f31e03..93b1e5b3 100644 --- a/.github/workflows/ai-insights.yml +++ b/.github/workflows/ai-review.yml @@ -18,10 +18,4 @@ jobs: steps: - name: PR Agent action step id: pragent - uses: Codium-ai/pr-agent@main - env: - OPENAI_KEY: ${{ secrets.OPENAI_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - github_action_config.auto_review: "true" - github_action_config.auto_describe: "true" - github_action_config.auto_improve: "true" + uses: coderabbitai/coderabbit-pr-review@v1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..ea05b8ae --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI Testing + +on: + push: + branches: [ "main", "master" ] + pull_request: + branches: [ "main", "master" ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + + - name: Pull Git LFS objects + run: git lfs pull + + - name: Install dependencies + run: | + python -m pip install --upgrade pip uv + uv pip install --system -e ".[dev,demo]" + + - name: Run Pytest + run: | + uv run pytest tests/ diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 6eeeb32c..9888c181 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -62,7 +62,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '24' + node-version: '22' cache: 'npm' cache-dependency-path: ./web/package-lock.json @@ -84,7 +84,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '24' + node-version: '22' - name: Install jscpd run: npm install -g jscpd diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..00645d46 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,40 @@ +name: "CodeQL Analysis" + +on: + push: + branches: [ "main", "master" ] + pull_request: + branches: [ "main", "master" ] + schedule: + - cron: '27 15 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python', 'javascript' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml new file mode 100644 index 00000000..2bde778d --- /dev/null +++ b/.github/workflows/greetings.yml @@ -0,0 +1,20 @@ +name: Greetings + +on: + pull_request_target: + types: [opened] + issues: + types: [opened] + +jobs: + greeting: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: 'Welcome to the EV Grid Oracle repository! Thank you for opening your first issue. Our team will review it shortly. Please ensure you have read our CONTRIBUTING.md.' + pr-message: 'Welcome to the EV Grid Oracle repository! Thank you for your first pull request. A maintainer will review it soon. Please ensure your PR follows our guidelines in CONTRIBUTING.md.' \ No newline at end of file diff --git a/.github/workflows/health-dashboard.yml b/.github/workflows/health-dashboard.yml index fb788a57..94b44724 100644 --- a/.github/workflows/health-dashboard.yml +++ b/.github/workflows/health-dashboard.yml @@ -45,10 +45,3 @@ jobs: with: name: health-dashboard path: dashboard_output/ - - - name: Deploy to GitHub Pages - if: github.ref == 'refs/heads/main' - uses: peaceiris/actions-gh-pages@v4 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./dashboard_output diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 00000000..fd05965c --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,15 @@ +name: "Pull Request Labeler" +on: +- pull_request_target + +jobs: + triage: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/labeler.yml \ No newline at end of file diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 00000000..c5c83d40 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,46 @@ +name: Deploy GitHub Pages + +on: + workflow_run: + workflows: ["Repository Health Dashboard"] + types: + - completed + branches: + - main + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: health-dashboard + path: ./public + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./public + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/repo-maintenance.yml b/.github/workflows/repo-maintenance.yml new file mode 100644 index 00000000..01030438 --- /dev/null +++ b/.github/workflows/repo-maintenance.yml @@ -0,0 +1,64 @@ +name: Repository Maintenance Automation + +on: + push: + branches: [ "main", "master" ] + pull_request: + branches: [ "main", "master" ] + schedule: + - cron: '0 2 * * *' # Daily at 2 AM UTC + +permissions: + contents: write + +jobs: + maintenance: + runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event_name == 'schedule' || github.event.pull_request.head.repo.full_name == github.repository + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref || github.ref }} + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip uv + uv pip install --system -e ".[dev,demo]" + uv pip install --system cyclonedx-bom + + - name: Auto-fix linting and formatting (Ruff) + run: | + uv run --with ruff ruff check --unsafe-fixes --fix . + uv run --with ruff ruff format . + + - name: Generate SBOM + run: | + uv run cyclonedx-py environment -o bom.json + + - name: Generate Documentation (Docs Sync) + run: | + python tools/docs_sync.py + + - name: Generate Architecture Diagram + run: | + python tools/generate_architecture_diagrams.py + + - name: Generate Knowledge Graph + run: | + python tools/generate_knowledge_graph.py + + - name: Commit changes + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + git add -f artifacts/ docs/api/ || true + git add . + git commit -m "chore(auto): repo maintenance (autofix, docs, graphs, sbom)" || echo "No changes to commit" + git push diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..45690471 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,21 @@ +name: Mark stale issues and pull requests + +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.' + stale-pr-message: 'This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.' + close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.' + close-pr-message: 'This PR was closed because it has been stalled for 7 days with no activity.' + days-before-stale: 60 + days-before-close: 7 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 07ce3c76..fa4f89cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,20 @@ .venv/ __pycache__/ *.pyc +.mypy_cache/ +.ruff_cache/ .pytest_cache/ *.egg-info/ .superpowers/ .cursor/* !.cursor/rules/ +# Large map files +web/public/maps/bangalore_roads_graph.json +web/public/maps/bangalore_roads_graph.json.gz +web/public/maps/bangalore_roads_full.geojson +web/public/maps/bangalore_roads_render.json + # Large generated artifacts (keep small plots committed) artifacts/frames*/ artifacts/*.mp4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..08cd2849 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.5 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format + +- repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + types_or: [javascript, jsx, ts, tsx, json, markdown] \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..6c85a138 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,49 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..84601d6f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# Contributing to EV Grid Oracle + +First off, thank you for considering contributing to EV Grid Oracle! It's people like you that make EV Grid Oracle such a great project. + +## Code of Conduct +By participating in this project, you are expected to uphold our Code of Conduct. Please read the `CODE_OF_CONDUCT.md` file before contributing. + +## How Can I Contribute? + +### Reporting Bugs +This section guides you through submitting a bug report for EV Grid Oracle. +- Use the issue tracker to report bugs. +- Please provide as much detail as possible to help us reproduce the bug. +- Check if the bug has already been reported. + +### Suggesting Enhancements +This section guides you through submitting an enhancement suggestion for EV Grid Oracle. +- Use the issue tracker to suggest enhancements. +- Describe the enhancement in detail and why it would be useful. + +### Pull Requests +1. Fork the repo and create your branch from `main`. +2. If you've added code that should be tested, add tests. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Make sure your code lints. +6. Issue that pull request! + +## Styleguides +### Git Commit Messages +- Use the present tense ("Add feature" not "Added feature"). +- Use the imperative mood ("Move cursor to..." not "Moves cursor to..."). +- Limit the first line to 72 characters or less. +- Reference issues and pull requests liberally after the first line. + +### Code Guidelines +- We use `ruff` for linting and formatting Python code. Please ensure your code passes `ruff check` and `ruff format`. +- We use `mypy` for static type checking. +- For JavaScript/TypeScript in the `web/` directory, we use `prettier` and `tsc`. diff --git a/ev_grid_oracle/bescom_feed.py b/ev_grid_oracle/bescom_feed.py index 470686ac..dc6945e2 100644 --- a/ev_grid_oracle/bescom_feed.py +++ b/ev_grid_oracle/bescom_feed.py @@ -85,7 +85,7 @@ def snapshot( return out def _stable_seed(self, *, seed: int, scenario: str, tick: int) -> int: - s = f"{seed}:{scenario}:{tick}".encode("utf-8") + s = f"{seed}:{scenario}:{tick}".encode() h = sha1(s, usedforsecurity=False).hexdigest()[:8] return int(h, 16) diff --git a/ev_grid_oracle/city_graph.py b/ev_grid_oracle/city_graph.py index a03a2a17..5cdb5d4b 100644 --- a/ev_grid_oracle/city_graph.py +++ b/ev_grid_oracle/city_graph.py @@ -2,7 +2,6 @@ from dataclasses import dataclass from math import asin, cos, radians, sin, sqrt -from typing import Optional import networkx as nx @@ -255,7 +254,7 @@ def build_city_graph() -> nx.Graph: if not nx.is_connected(g): # Fail fast: graph must be connected for routing to work. - comps = [sorted(list(c)) for c in nx.connected_components(g)] + comps = [sorted(c) for c in nx.connected_components(g)] raise RuntimeError(f"city graph not connected, components={comps}") return g @@ -266,7 +265,7 @@ def travel_time_minutes( from_station_id: str, to_station_id: str, *, - default_if_missing: Optional[float] = None, + default_if_missing: float | None = None, ) -> float: if from_station_id == to_station_id: return 0.0 diff --git a/ev_grid_oracle/env.py b/ev_grid_oracle/env.py index 149ead44..9f21163a 100644 --- a/ev_grid_oracle/env.py +++ b/ev_grid_oracle/env.py @@ -2,10 +2,10 @@ from dataclasses import dataclass, field from random import Random -from typing import Optional import networkx as nx +from .bescom_feed import BESCOMFeedAPI from .city_graph import STATIONS from .demand_sim import sample_arrivals_per_step from .grid_sim import update_grid_load @@ -19,10 +19,9 @@ PeakRisk, StationState, ) +from .personas import FleetMode, choose_persona from .reward import compute_reward from .reward_hack import RewardHackDetector -from .bescom_feed import BESCOMFeedAPI -from .personas import FleetMode, choose_persona from .scenarios import ( ScenarioEvent, ScenarioModifiers, @@ -46,7 +45,7 @@ class EVGridCore: max_steps: int = 48 step_minutes: int = 5 rng: Random = field(default_factory=Random) - _grid_state: Optional[GridState] = None + _grid_state: GridState | None = None scenario: ScenarioName = "baseline" _scenario_schedule: list[ScenarioEvent] = field(default_factory=list) _scenario_mods: ScenarioModifiers = field(default_factory=ScenarioModifiers) @@ -59,7 +58,7 @@ class EVGridCore: def reset( self, *, - seed: Optional[int] = None, + seed: int | None = None, scenario: ScenarioName = "baseline", fleet_mode: FleetMode = "mixed", ) -> EVGridObservation: @@ -180,8 +179,7 @@ def step(self, action: EVGridAction) -> EVGridObservation: new_total = self._scenario_mods.slot_derate.get(s.station_id) if new_total is not None and new_total < s.total_slots: s.total_slots = int(new_total) - if s.occupied_slots > s.total_slots: - s.occupied_slots = s.total_slots + s.occupied_slots = min(s.occupied_slots, s.total_slots) # 1) apply action (deterministic validation + state mutation) action_effect = _apply_action(prev_state, action) @@ -197,7 +195,7 @@ def step(self, action: EVGridAction) -> EVGridObservation: arrivals = sample_arrivals_per_step( self.rng, prev_state.hour, day_type=prev_state.day_type.value ) - arrivals = int(round(arrivals * float(self._scenario_mods.arrivals_mult))) + arrivals = round(arrivals * float(self._scenario_mods.arrivals_mult)) for _ in range(arrivals): prev_state.pending_evs.append( _make_ev( diff --git a/ev_grid_oracle/grid_sim.py b/ev_grid_oracle/grid_sim.py index 150ead29..373c29ce 100644 --- a/ev_grid_oracle/grid_sim.py +++ b/ev_grid_oracle/grid_sim.py @@ -15,7 +15,7 @@ class GridParams: def _clamp01(x: float) -> float: - return 0.0 if x < 0.0 else 1.0 if x > 1.0 else x + return 0.0 if x < 0.0 else min(x, 1.0) def baseline_grid_load( diff --git a/ev_grid_oracle/models.py b/ev_grid_oracle/models.py index 02201842..02498d04 100644 --- a/ev_grid_oracle/models.py +++ b/ev_grid_oracle/models.py @@ -1,10 +1,9 @@ from __future__ import annotations from enum import Enum -from typing import Any, Literal, Optional +from typing import Any, Literal from openenv.core.env_server.types import Action, Observation - from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator @@ -110,20 +109,19 @@ class EVGridAction(Action): action_type: ActionType ev_id: str - station_id: Optional[str] = None + station_id: str | None = None charge_rate: ChargeRate = ChargeRate.fast defer_minutes: int = Field(0, ge=0) @model_validator(mode="after") - def _check_consistency(self) -> "EVGridAction": + def _check_consistency(self) -> EVGridAction: if self.action_type == ActionType.route: if not self.station_id: raise ValueError("station_id required when action_type='route'") if self.defer_minutes != 0: raise ValueError("defer_minutes must be 0 when action_type='route'") - if self.action_type == ActionType.defer: - if self.defer_minutes <= 0: - raise ValueError("defer_minutes must be > 0 when action_type='defer'") + if self.action_type == ActionType.defer and self.defer_minutes <= 0: + raise ValueError("defer_minutes must be > 0 when action_type='defer'") if self.action_type == ActionType.load_shift: # For v1: still tie action to an EV (ev_id) but station optional. if self.defer_minutes != 0: diff --git a/ev_grid_oracle/oracle_agent.py b/ev_grid_oracle/oracle_agent.py index cc0503de..2b86eb72 100644 --- a/ev_grid_oracle/oracle_agent.py +++ b/ev_grid_oracle/oracle_agent.py @@ -1,14 +1,13 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Optional, Tuple, cast +from typing import Any, cast from ev_grid_oracle.models import EVGridAction, GridState from ev_grid_oracle.parsing import parse_action from ev_grid_oracle.policies import baseline_policy - -_CACHE: dict[tuple[str, Optional[str]], tuple[Any, Any]] = {} +_CACHE: dict[tuple[str, str | None], tuple[Any, Any]] = {} _CACHE_LOCK = None @@ -69,7 +68,7 @@ class OracleAgent: Optional: load a trained LoRA adapter when `lora_repo_id` provided. """ - lora_repo_id: Optional[str] = None + lora_repo_id: str | None = None base_model_id: str = "unsloth/Qwen2.5-3B-Instruct" max_new_tokens: int = 140 @@ -129,7 +128,7 @@ def act(self, state: GridState, prompt: str, graph) -> EVGridAction: def act_with_text( self, state: GridState, prompt: str, graph - ) -> Tuple[EVGridAction, str]: + ) -> tuple[EVGridAction, str]: # choose target ev_id (matches env prompt builder v0) ev_id = state.pending_evs[0].ev_id if state.pending_evs else "EV-000" diff --git a/ev_grid_oracle/parsing.py b/ev_grid_oracle/parsing.py index daeaa81d..320ea1ec 100644 --- a/ev_grid_oracle/parsing.py +++ b/ev_grid_oracle/parsing.py @@ -1,17 +1,15 @@ from __future__ import annotations import re -from typing import Optional, Tuple from .models import ( ActionType, ChargeRate, EVGridAction, - SimulationPrediction, SimTopStation, + SimulationPrediction, ) - ACTION_RE = re.compile( r"ACTION:\s*(?Proute|defer|load_shift)\s*\n" r"STATION:\s*(?PBLR-\d\d|NONE)\s*\n" @@ -30,7 +28,7 @@ ) -def parse_simulation(text: str) -> Optional[SimulationPrediction]: +def parse_simulation(text: str) -> SimulationPrediction | None: m = SIM_RE.search(text) if not m: return None @@ -58,7 +56,7 @@ def parse_simulation(text: str) -> Optional[SimulationPrediction]: return None -def parse_action(text: str, *, ev_id: str) -> Optional[EVGridAction]: +def parse_action(text: str, *, ev_id: str) -> EVGridAction | None: m = ACTION_RE.search(text.strip()) if not m: return None @@ -84,7 +82,7 @@ def parse_action(text: str, *, ev_id: str) -> Optional[EVGridAction]: def parse_simulation_and_action( text: str, *, ev_id: str -) -> Tuple[Optional[SimulationPrediction], Optional[EVGridAction]]: +) -> tuple[SimulationPrediction | None, EVGridAction | None]: """ Parse both dream prediction and action (either can be missing). """ diff --git a/ev_grid_oracle/personas.py b/ev_grid_oracle/personas.py index eca963b8..23854148 100644 --- a/ev_grid_oracle/personas.py +++ b/ev_grid_oracle/personas.py @@ -4,7 +4,6 @@ from random import Random from typing import Literal - FleetPersona = Literal[ "TaxiFleet", "CorporateShuttle", "DeliveryFleet", "PrivateOwner", "Emergency" ] diff --git a/ev_grid_oracle/reward.py b/ev_grid_oracle/reward.py index cd2f7ce8..777e227e 100644 --- a/ev_grid_oracle/reward.py +++ b/ev_grid_oracle/reward.py @@ -1,5 +1,6 @@ from __future__ import annotations +import itertools from dataclasses import dataclass from statistics import mean @@ -62,7 +63,7 @@ def _graph_route_km( path = nx.shortest_path(city_graph, n1, n2, weight="km") total = 0.0 - for a, b in zip(path, path[1:]): + for a, b in itertools.pairwise(path): w = city_graph[a][b].get("km") if w is None: total += _haversine_km( diff --git a/ev_grid_oracle/road_models.py b/ev_grid_oracle/road_models.py index 19953089..4ab8013e 100644 --- a/ev_grid_oracle/road_models.py +++ b/ev_grid_oracle/road_models.py @@ -1,6 +1,5 @@ from __future__ import annotations - from openenv.core.env_server.types import Action, Observation from pydantic import BaseModel, ConfigDict, Field, model_validator @@ -17,7 +16,7 @@ class RoadAction(Action): next_node: int = Field(..., ge=0) @model_validator(mode="after") - def _non_trivial(self) -> "RoadAction": + def _non_trivial(self) -> RoadAction: if self.current_node == self.next_node: raise ValueError("next_node must differ from current_node") return self diff --git a/ev_grid_oracle/scenarios.py b/ev_grid_oracle/scenarios.py index 22834898..ece3bb72 100644 --- a/ev_grid_oracle/scenarios.py +++ b/ev_grid_oracle/scenarios.py @@ -3,7 +3,6 @@ from dataclasses import dataclass from typing import Any, Literal, TypedDict - ScenarioName = Literal[ "baseline", "heatwave_peak", @@ -188,7 +187,7 @@ def apply_scenario_events( # Stable ids for bookmarks / UI (deterministic). for e in fired: - e.setdefault("id", f"{name}:{int(e['tick'])}:{str(e.get('type', ''))}") + e.setdefault("id", f"{name}:{int(e['tick'])}:{e.get('type', '')!s}") # Modifiers are "sticky": once an event changes a knob, it persists. for e in fired: diff --git a/ev_grid_oracle/traffic.py b/ev_grid_oracle/traffic.py index b8b74a1d..327ee4ac 100644 --- a/ev_grid_oracle/traffic.py +++ b/ev_grid_oracle/traffic.py @@ -6,7 +6,7 @@ def _clamp(x: float, lo: float, hi: float) -> float: - return lo if x < lo else hi if x > hi else x + return lo if x < lo else min(x, hi) def _stable_u01(*parts: object) -> float: diff --git a/ev_grid_oracle/world_model_verifier.py b/ev_grid_oracle/world_model_verifier.py index e0cb5894..af566423 100644 --- a/ev_grid_oracle/world_model_verifier.py +++ b/ev_grid_oracle/world_model_verifier.py @@ -95,7 +95,7 @@ def score_prediction( overlap_score = max(0.0, 1.0 - overlap_err) score = 0.40 * grid_score + 0.25 * ren_score + 0.20 * jacc + 0.15 * overlap_score - score = 0.0 if score < 0.0 else 1.0 if score > 1.0 else score + score = 0.0 if score < 0.0 else min(score, 1.0) return PredictionScore( score_0_1=float(score), breakdown={ diff --git a/server/app.py b/server/app.py index 3e14b3f3..6fa2174f 100644 --- a/server/app.py +++ b/server/app.py @@ -1,14 +1,15 @@ from __future__ import annotations import concurrent.futures +import hashlib +import logging import os -from pathlib import Path import time from collections import OrderedDict -import logging -from server.road_router import get_router +from pathlib import Path + from ev_grid_oracle.traffic import TrafficModel -import hashlib +from server.road_router import get_router try: from openenv.core.env_server.http_server import create_app @@ -17,43 +18,43 @@ from typing import Any, Literal, cast from uuid import uuid4 -from pydantic import BaseModel, Field +import networkx as nx from fastapi import Body, HTTPException, Query, Request from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel, Field -from ev_grid_oracle.city_graph import build_city_graph, _BY_ID, _BY_SLUG -import networkx as nx +from ev_grid_oracle.city_graph import _BY_ID, _BY_SLUG, build_city_graph from ev_grid_oracle.env import EVGridCore, _build_prompt from ev_grid_oracle.models import ( ActionType, - EVRequest, EVGridAction, EVGridObservation, + EVRequest, GridDirective, MultiAgentStepRequest, NegotiationMessage, ) +from ev_grid_oracle.multi_agent import MultiAgentSession from ev_grid_oracle.oracle_agent import OracleAgent -from ev_grid_oracle.policies import baseline_policy from ev_grid_oracle.parsing import parse_simulation +from ev_grid_oracle.policies import baseline_policy from ev_grid_oracle.reward import split_role_rewards +from ev_grid_oracle.road_models import RoadAction, RoadObservation from ev_grid_oracle.scenarios import ScenarioName from ev_grid_oracle.world_model_verifier import ( rollout_deterministic_5ticks, score_prediction, ) -from ev_grid_oracle.multi_agent import MultiAgentSession from server.ev_grid_environment import EVGridEnvironment from server.ev_grid_road_environment import EVGridRoadEnvironment -from ev_grid_oracle.road_models import RoadAction, RoadObservation +from server.road_router import haversine_m from server.role_metrics import ( compute_role_kpis, compute_role_reward_breakdown, summarize_action, ) -from server.road_router import haversine_m log = logging.getLogger("ev-grid-oracle") if not log.handlers: @@ -249,7 +250,7 @@ def healthz(req: Request) -> dict[str, Any]: _DEMO_MAX_SESSIONS = int(os.getenv("DEMO_MAX_SESSIONS", "64")) # Ordered for deterministic eviction of oldest sessions. -_demo_sessions: "OrderedDict[str, tuple[float, EVGridCore]]" = OrderedDict() +_demo_sessions: OrderedDict[str, tuple[float, EVGridCore]] = OrderedDict() _demo_graph = build_city_graph() _SIM_VERSION = "2026-04-26.1" @@ -361,7 +362,7 @@ def _demo_session_get(session_id: str) -> EVGridCore | None: row = _demo_sessions.get(session_id) if row is None: return None - ts, core = row + _ts, core = row # touch (LRU-ish) _demo_sessions.move_to_end(session_id, last=True) _demo_sessions[session_id] = (time.time(), core) @@ -383,7 +384,7 @@ class DemoNewRequest(BaseModel): _MA_SESSION_TTL_SEC = int(os.getenv("MA_SESSION_TTL_SEC", "3600")) _MA_MAX_SESSIONS = int(os.getenv("MA_MAX_SESSIONS", "64")) -_ma_sessions: "OrderedDict[str, tuple[float, MultiAgentSession]]" = OrderedDict() +_ma_sessions: OrderedDict[str, tuple[float, MultiAgentSession]] = OrderedDict() def _ma_gc(now: float | None = None) -> None: @@ -524,7 +525,7 @@ def ma_auto_step( text="Routing using heuristic baseline under grid constraints.", ) else: - action, _txt, active, timed_out, skipped = _demo_oracle_act_with_guard( + action, _txt, active, _timed_out, _skipped = _demo_oracle_act_with_guard( st=st, core=sess.core, oracle_lora_repo=payload.oracle_lora_repo ) fleet_action = action @@ -1159,7 +1160,7 @@ def demo_step( seed_i = int(core._seed_for_bescom) scen = str(core.scenario) h = hashlib.sha1( - f"{seed_i}|{scen}|{mode}|ambient|{tick_i}".encode("utf-8"), + f"{seed_i}|{scen}|{mode}|ambient|{tick_i}".encode(), usedforsecurity=False, ).digest() a_i = int.from_bytes(h[:2], "big") % len(st.stations) diff --git a/server/road_router.py b/server/road_router.py index ee12289c..d77fb352 100644 --- a/server/road_router.py +++ b/server/road_router.py @@ -1,11 +1,12 @@ from __future__ import annotations -import json import gzip +import itertools +import json +from collections.abc import Callable from dataclasses import dataclass from math import asin, cos, radians, sin, sqrt from pathlib import Path -from typing import Callable, Optional import networkx as nx @@ -61,7 +62,7 @@ class RoadRouter: edge_geom: dict[tuple[int, int], list[list[float]]] @classmethod - def load(cls, path: Path) -> "RoadRouter": + def load(cls, path: Path) -> RoadRouter: if str(path).endswith(".gz"): with gzip.open(path, "rb") as f: obj = json.loads(f.read().decode("utf-8")) @@ -121,7 +122,7 @@ def route_polyline( dst_lng: float, traffic: TrafficModel | None = None, tick: int | None = None, - ) -> Optional[tuple[list[list[float]], list[int]]]: + ) -> tuple[list[list[float]], list[int]] | None: a = self.nearest_node(lat=src_lat, lng=src_lng) b = self.nearest_node(lat=dst_lat, lng=dst_lng) try: @@ -150,7 +151,7 @@ def _w(u: int, v: int, attrs: dict) -> float: return None poly: list[list[float]] = [] seg_m_q: list[int] = [] - for u, v in zip(path, path[1:]): + for u, v in itertools.pairwise(path): seg = self.edge_geom.get((int(u), int(v))) # Edge traffic multiplier used for this segment (quantized). if traffic is not None and tick is not None: diff --git a/server/role_metrics.py b/server/role_metrics.py index 3ec1081d..f82f356b 100644 --- a/server/role_metrics.py +++ b/server/role_metrics.py @@ -95,7 +95,7 @@ def part(key: str) -> float: } for k in keys: wmap = weights.get(k, {}) - for r in out.keys(): + for r in out: out[r][k] = float(comps[k]) * float(wmap.get(r, 0.0)) totals: dict[Role, float] = {} diff --git a/tools/build_road_graph.py b/tools/build_road_graph.py index a68236ff..4fc98120 100644 --- a/tools/build_road_graph.py +++ b/tools/build_road_graph.py @@ -1,9 +1,10 @@ from __future__ import annotations import argparse -import json -import hashlib import gzip +import hashlib +import itertools +import json from dataclasses import dataclass from math import asin, cos, radians, sin, sqrt from pathlib import Path @@ -11,7 +12,6 @@ import networkx as nx - ROOT = Path(__file__).resolve().parents[1] @@ -51,8 +51,8 @@ def encode_polyline_latlng(points: list[list[float]], *, precision: int = 5) -> prev_lng = 0 out = "" for lat, lng in points: - ilat = int(round(float(lat) * factor)) - ilng = int(round(float(lng) * factor)) + ilat = round(float(lat) * factor) + ilng = round(float(lng) * factor) out += _encode_signed(ilat - prev_lat) out += _encode_signed(ilng - prev_lng) prev_lat = ilat @@ -154,7 +154,7 @@ def add_neighbor(a: tuple[float, float], b: tuple[float, float]): if len(pts) < 2: continue snapped = [snap(lat, lng, decimals=snap_decimals) for (lat, lng) in pts] - for a, b in zip(snapped, snapped[1:]): + for a, b in itertools.pairwise(snapped): add_neighbor(a, b) return {k: (len(v) != 2) for k, v in adj.items()} @@ -221,7 +221,7 @@ def flush(to_k: tuple[float, float]): # Distance along the segment geometry dist_m = 0.0 - for (la1, lo1), (la2, lo2) in zip(seg_geom, seg_geom[1:]): + for (la1, lo1), (la2, lo2) in itertools.pairwise(seg_geom): dist_m += haversine_m(float(la1), float(lo1), float(la2), float(lo2)) v_kmh = speed_kmh(highway) travel_s = dist_m / max(1e-3, (v_kmh * 1000.0 / 3600.0)) @@ -277,7 +277,7 @@ def filter_largest_component( g3.add_edge(int(e["a"]), int(e["b"])) comps3 = list(nx.connected_components(g3)) keep_nodes = max(comps3, key=lambda c: len(c)) if comps3 else set() - keep_nodes_set = set(int(x) for x in keep_nodes) + keep_nodes_set = {int(x) for x in keep_nodes} # Remap nodes to a compact id space. id_map: dict[int, int] = {} diff --git a/tools/build_roads_render.py b/tools/build_roads_render.py index f40f3ded..a271d8a0 100644 --- a/tools/build_roads_render.py +++ b/tools/build_roads_render.py @@ -5,7 +5,6 @@ from pathlib import Path from typing import Any - ROOT = Path(__file__).resolve().parents[1] diff --git a/tools/docs_sync.py b/tools/docs_sync.py new file mode 100644 index 00000000..8071337d --- /dev/null +++ b/tools/docs_sync.py @@ -0,0 +1,74 @@ +import ast +import os +from pathlib import Path + + +def generate_markdown(file_path: Path) -> str: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + try: + tree = ast.parse(content) + except SyntaxError: + return f"# Error parsing {file_path.name}\n\nCould not parse this file as Python code." + + module_docstring = ast.get_docstring(tree) + + md = [f"# Module: `{file_path.name}`\n"] + if module_docstring: + md.append(f"{module_docstring}\n") + + for node in tree.body: + if isinstance(node, ast.ClassDef): + md.append(f"## Class: `{node.name}`") + class_doc = ast.get_docstring(node) + if class_doc: + md.append(f"\n{class_doc}\n") + + for item in node.body: + if isinstance(item, ast.FunctionDef): + md.append(f"### Method: `{item.name}`") + method_doc = ast.get_docstring(item) + if method_doc: + md.append(f"\n{method_doc}\n") + + elif isinstance(node, ast.FunctionDef): + md.append(f"## Function: `{node.name}`") + func_doc = ast.get_docstring(node) + if func_doc: + md.append(f"\n{func_doc}\n") + + return "\n".join(md) + + +def main(): + root_dir = Path(".") + docs_api_dir = root_dir / "docs" / "api" + docs_api_dir.mkdir(parents=True, exist_ok=True) + + for root, dirs, files in os.walk(root_dir): + # Ignore hidden system directories + dirs[:] = [d for d in dirs if not d.startswith(".")] + + # Explicitly ignore standard virtual env or build folders + ignore_dirs = ["venv", "build", "dist", "node_modules"] + dirs[:] = [d for d in dirs if d not in ignore_dirs] + + for file in files: + if file.endswith(".py"): + file_path = Path(root) / file + rel_path = file_path.relative_to(root_dir) + + # Construct path-safe filename incorporating relative path + safe_name = str(rel_path).replace(os.sep, "_").replace(".py", ".md") + out_path = docs_api_dir / safe_name + + markdown_content = generate_markdown(file_path) + with open(out_path, "w", encoding="utf-8") as out_f: + out_f.write(markdown_content) + + print(f"Generated API documentation in {docs_api_dir}") + + +if __name__ == "__main__": + main() diff --git a/tools/fetch_bangalore_roads_overpass.py b/tools/fetch_bangalore_roads_overpass.py index 4fdaa966..120d815c 100644 --- a/tools/fetch_bangalore_roads_overpass.py +++ b/tools/fetch_bangalore_roads_overpass.py @@ -8,7 +8,6 @@ from pathlib import Path from typing import Any - ROOT = Path(__file__).resolve().parents[1] @@ -75,7 +74,7 @@ def _http_post(url: str, data: dict[str, str], *, retries: int = 3) -> bytes: try: with urllib.request.urlopen(req, timeout=240) as r: # nosec B310 return r.read() - except Exception as e: # noqa: BLE001 + except Exception as e: last_err = e if attempt >= retries: raise diff --git a/tools/fetch_osm_roads.py b/tools/fetch_osm_roads.py index e8fcd1d4..b2fb3bb5 100644 --- a/tools/fetch_osm_roads.py +++ b/tools/fetch_osm_roads.py @@ -8,7 +8,6 @@ from dataclasses import dataclass from pathlib import Path - OVERPASS_URL = "https://overpass-api.de/api/interpreter" diff --git a/tools/generate_architecture_diagrams.py b/tools/generate_architecture_diagrams.py new file mode 100644 index 00000000..d854842f --- /dev/null +++ b/tools/generate_architecture_diagrams.py @@ -0,0 +1,63 @@ +import ast +import json +import os +from pathlib import Path +from typing import Any + + +def main(): + root_dir = Path(".") + artifacts_dir = root_dir / "artifacts" + artifacts_dir.mkdir(parents=True, exist_ok=True) + + graph: dict[str, list[dict[str, Any]]] = {"nodes": [], "edges": []} + + for root, dirs, files in os.walk(root_dir): + # Ignore hidden system directories + dirs[:] = [d for d in dirs if not d.startswith(".")] + + # Explicitly ignore standard virtual env or build folders + ignore_dirs = ["venv", "build", "dist", "node_modules"] + dirs[:] = [d for d in dirs if d not in ignore_dirs] + + for file in files: + if file.endswith(".py"): + file_path = Path(root) / file + rel_path = str(file_path.relative_to(root_dir)) + + graph["nodes"].append({"id": rel_path, "type": "file"}) + + try: + with open(file_path, "r", encoding="utf-8") as f: + tree = ast.parse(f.read()) + except Exception: + continue # nosec B112 + + for node in tree.body: + if isinstance(node, ast.Import): + for name in node.names: + graph["edges"].append( + { + "source": rel_path, + "target": name.name, + "type": "imports", + } + ) + elif isinstance(node, ast.ImportFrom) and node.module: + graph["edges"].append( + { + "source": rel_path, + "target": node.module, + "type": "imports_from", + } + ) + + out_path = artifacts_dir / "architecture_graph.json" + with open(out_path, "w", encoding="utf-8") as f: + json.dump(graph, f, indent=2) + + print(f"Generated Architecture Diagram graph to {out_path}") + + +if __name__ == "__main__": + main() diff --git a/tools/generate_health_dashboard.py b/tools/generate_health_dashboard.py index cf321893..39fa7725 100644 --- a/tools/generate_health_dashboard.py +++ b/tools/generate_health_dashboard.py @@ -1,7 +1,8 @@ import json -import subprocess # nosec B404 import os +import subprocess # nosec B404 from datetime import datetime, timezone + from jinja2 import Environment, FileSystemLoader # Extract sensitive variables immediately to prevent child processes @@ -269,7 +270,6 @@ def generate_ai_insights(scores, complexity, vulns, lint_errors): except Exception as e: print(f"Failed to generate AI insights via OpenAI: {e}") # Fallback to static insights on error - pass # Static Fallback insights = [] diff --git a/tools/generate_knowledge_graph.py b/tools/generate_knowledge_graph.py new file mode 100644 index 00000000..686e000c --- /dev/null +++ b/tools/generate_knowledge_graph.py @@ -0,0 +1,79 @@ +import ast +import json +import os +from pathlib import Path +from typing import Any + + +def main(): + root_dir = Path(".") + artifacts_dir = root_dir / "artifacts" + artifacts_dir.mkdir(parents=True, exist_ok=True) + + knowledge: dict[str, list[dict[str, Any]]] = {"entities": [], "relationships": []} + + for root, dirs, files in os.walk(root_dir): + # Ignore hidden system directories + dirs[:] = [d for d in dirs if not d.startswith(".")] + + # Explicitly ignore standard virtual env or build folders + ignore_dirs = ["venv", "build", "dist", "node_modules"] + dirs[:] = [d for d in dirs if d not in ignore_dirs] + + for file in files: + if file.endswith(".py"): + file_path = Path(root) / file + rel_path = str(file_path.relative_to(root_dir)) + + knowledge["entities"].append( + {"id": rel_path, "type": "file", "name": file} + ) + + try: + with open(file_path, "r", encoding="utf-8") as f: + tree = ast.parse(f.read()) + except Exception: + continue # nosec B112 + + for node in tree.body: + if isinstance(node, ast.ClassDef): + class_id = f"{rel_path}:{node.name}" + knowledge["entities"].append( + {"id": class_id, "type": "class", "name": node.name} + ) + knowledge["relationships"].append( + {"source": rel_path, "target": class_id, "type": "contains"} + ) + + for item in node.body: + if isinstance(item, ast.FunctionDef): + func_id = f"{class_id}:{item.name}" + knowledge["entities"].append( + {"id": func_id, "type": "method", "name": item.name} + ) + knowledge["relationships"].append( + { + "source": class_id, + "target": func_id, + "type": "has_method", + } + ) + + elif isinstance(node, ast.FunctionDef): + func_id = f"{rel_path}:{node.name}" + knowledge["entities"].append( + {"id": func_id, "type": "function", "name": node.name} + ) + knowledge["relationships"].append( + {"source": rel_path, "target": func_id, "type": "contains"} + ) + + out_path = artifacts_dir / "knowledge_graph.json" + with open(out_path, "w", encoding="utf-8") as f: + json.dump(knowledge, f, indent=2) + + print(f"Generated Knowledge Graph to {out_path}") + + +if __name__ == "__main__": + main() diff --git a/tools/road_reward_smoke.py b/tools/road_reward_smoke.py index 41932b73..37bea28c 100644 --- a/tools/road_reward_smoke.py +++ b/tools/road_reward_smoke.py @@ -10,12 +10,12 @@ def main() -> int: core = RoadCore(g=None, nodes=[]) obs = core.reset(seed=0) st = obs.state - nb = list(core.g.neighbors(st.node))[0] + nb = next(iter(core.g.neighbors(st.node))) ok = f"CURRENT_NODE: {st.node}\nNEXT_NODE: {int(nb)}\nREASON: go\nCONFIDENCE: 0.7\n" bad = f"CURRENT_NODE: {st.node + 999}\nNEXT_NODE: {int(nb)}\nREASON: hack\nCONFIDENCE: 0.7\n" - r = re.compile(r"CURRENT_NODE:\s*(\d+)\s*\nNEXT_NODE:\s*(\d+)\s*\n", re.I) + r = re.compile(r"CURRENT_NODE:\s*(\d+)\s*\nNEXT_NODE:\s*(\d+)\s*\n", re.IGNORECASE) def parse(t: str) -> RoadAction | None: m = r.search(t.strip()) diff --git a/training/train_grpo.ipynb b/training/train_grpo.ipynb index ef88aea4..f6ac6ae4 100644 --- a/training/train_grpo.ipynb +++ b/training/train_grpo.ipynb @@ -109,14 +109,12 @@ "outputs": [], "source": [ "import re\n", - "from typing import Optional\n", "\n", "from datasets import Dataset\n", "\n", "from ev_grid_oracle.road_env import RoadCore\n", "from ev_grid_oracle.road_models import RoadAction, RoadState\n", "\n", - "\n", "core = RoadCore(g=None, nodes=[]) # graph is loaded inside reset()" ] }, @@ -134,7 +132,7 @@ ")\n", "\n", "\n", - "def parse_action(text: str) -> Optional[RoadAction]:\n", + "def parse_action(text: str) -> RoadAction | None:\n", " m = ACTION_RE.search(text.strip())\n", " if not m:\n", " return None\n", diff --git a/viz/city_map.py b/viz/city_map.py index ac0b9907..61b34c18 100644 --- a/viz/city_map.py +++ b/viz/city_map.py @@ -2,7 +2,6 @@ import math from dataclasses import dataclass -from typing import Optional import pygame @@ -28,7 +27,7 @@ def _norm(v: float, lo: float, hi: float) -> float: if hi <= lo: return 0.0 x = (v - lo) / (hi - lo) - return 0.0 if x < 0.0 else 1.0 if x > 1.0 else x + return 0.0 if x < 0.0 else min(x, 1.0) @dataclass @@ -91,7 +90,7 @@ def render( self, surf: pygame.Surface, *, - last_action: Optional[EVGridAction] = None, + last_action: EVGridAction | None = None, mode_label: str = "", ): cfg = self.cfg @@ -255,26 +254,25 @@ def run_live(seed: int = 123, *, mode: str = "baseline"): env.reset(seed=seed) renderer = CityMapRenderer(env, cfg) - last_action: Optional[EVGridAction] = None + last_action: EVGridAction | None = None running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False - if event.type == pygame.KEYDOWN: - if event.key == pygame.K_SPACE: - # one sim tick - st = env._grid_state - if st is None or not st.pending_evs: - action = EVGridAction( - action_type=ActionType.load_shift, - ev_id="EV-000", - defer_minutes=0, - ) - else: - action = baseline_policy(st, env.city_graph) - last_action = action - env.step(action) + if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: + # one sim tick + st = env._grid_state + if st is None or not st.pending_evs: + action = EVGridAction( + action_type=ActionType.load_shift, + ev_id="EV-000", + defer_minutes=0, + ) + else: + action = baseline_policy(st, env.city_graph) + last_action = action + env.step(action) renderer.render(screen, last_action=last_action, mode_label=mode) pygame.display.flip() diff --git a/viz/gradio_demo.py b/viz/gradio_demo.py index 26dbfa24..3b9d3fb9 100644 --- a/viz/gradio_demo.py +++ b/viz/gradio_demo.py @@ -13,7 +13,6 @@ from ev_grid_oracle.policies import baseline_policy from training.evaluate import run_episode, summarize - Mode = Literal["Untrained Baseline", "Oracle Agent"] @@ -21,7 +20,7 @@ def _norm(v: float, lo: float, hi: float) -> float: if hi <= lo: return 0.0 x = (v - lo) / (hi - lo) - return 0.0 if x < 0.0 else 1.0 if x > 1.0 else x + return 0.0 if x < 0.0 else min(x, 1.0) def _station_color(load_pct: float) -> tuple[int, int, int]: diff --git a/viz/record.py b/viz/record.py index 2f0fbc01..d05caf7d 100644 --- a/viz/record.py +++ b/viz/record.py @@ -2,7 +2,6 @@ import argparse from pathlib import Path -from typing import Optional import pygame @@ -37,7 +36,7 @@ def record( env.reset(seed=seed) renderer = CityMapRenderer(env, cfg) - last_action: Optional[EVGridAction] = None + last_action: EVGridAction | None = None frame = 0 for step in range(steps): st = env._grid_state diff --git a/viz/record_two_phase.py b/viz/record_two_phase.py index ac800cef..99402b74 100644 --- a/viz/record_two_phase.py +++ b/viz/record_two_phase.py @@ -1,8 +1,8 @@ from __future__ import annotations import argparse +from collections.abc import Callable from pathlib import Path -from typing import Callable, Optional import pygame @@ -13,7 +13,6 @@ from ev_grid_oracle.policies import baseline_policy from viz.city_map import CityMapRenderer, RenderConfig - PolicyFn = Callable[[GridState, object], EVGridAction] @@ -38,7 +37,7 @@ def record_phase( frame_start: int, policy: PolicyFn, ) -> int: - last_action: Optional[EVGridAction] = None + last_action: EVGridAction | None = None frame = frame_start for _ in range(steps): last_action = _step_action(env, policy)