build(agent): new-agents#a6e6ec iteration
This commit is contained in:
parent
0b6e7311ba
commit
dd13bf419b
|
|
@ -0,0 +1,21 @@
|
||||||
|
node_modules/
|
||||||
|
.npmrc
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
__tests__/
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.cache/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
tmp/
|
||||||
|
.tmp/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
*.egg-info/
|
||||||
|
.pytest_cache/
|
||||||
|
READY_TO_PUBLISH
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Architecture, testing, and contribution guide for OpenEnergySphere EnergiBridge MVP.
|
||||||
|
|
||||||
|
Overview
|
||||||
|
- A production-ready Python-based MVP that enables cross-domain energy planning with privacy-first primitives.
|
||||||
|
- Core concepts (OpenEnergySphere analogies): Objects (LocalProblems), Morphisms (SharedVariables), PlanDelta, and a registry of Graph-of-Contracts. Adapters implement plug-and-play data/model interfaces.
|
||||||
|
- EnergiBridge maps OpenEnergySphere primitives to canonical IR concepts and enables cross-domain adapters with offline-first capabilities.
|
||||||
|
|
||||||
|
Tech Stack (current MVP)
|
||||||
|
- Language: Python 3.10+
|
||||||
|
- Web API: FastAPI
|
||||||
|
- Data Modeling: Pydantic
|
||||||
|
- Storage: In-memory registry (extensible to SQLite/PostgreSQL)
|
||||||
|
- Packaging: pyproject.toml with setuptools build backend
|
||||||
|
- Testing: pytest
|
||||||
|
|
||||||
|
Project Structure (high level)
|
||||||
|
- energysphere/ - Python package with models, registry, sdk, adapters, and server.
|
||||||
|
- energysphere/server/ - FastAPI app exposing contract/adapter endpoints.
|
||||||
|
- energysphere/adapters/ - Starter adapters (substation_meter, der_aggregator).
|
||||||
|
- tests/ - Pytest unit tests for registry and adapters.
|
||||||
|
- test.sh - Test runner including packaging build step.
|
||||||
|
- AGENTS.md - This document.
|
||||||
|
|
||||||
|
How to run locally
|
||||||
|
- Install dependencies (in a virtualenv):
|
||||||
|
- python -m pip install --upgrade pip setuptools wheel
|
||||||
|
- pip install -r requirements.txt (if you add one later) or rely on pyproject tooling
|
||||||
|
- Run tests:
|
||||||
|
- bash test.sh
|
||||||
|
- Run the API (in dev):
|
||||||
|
- uvicorn energysphere.server.main:app --reload --port 8000
|
||||||
|
|
||||||
|
Development workflow
|
||||||
|
- Implement minimal MVP features in small, testable units.
|
||||||
|
- Add tests for all new functionality and ensure test.sh passes before merging.
|
||||||
|
- Extend registry with persistent storage and implement authentication and governance ledger in future iterations.
|
||||||
|
|
||||||
|
Contribution rules
|
||||||
|
- All changes should pass tests (pytest) and packaging build (python -m build).
|
||||||
|
- Follow existing naming conventions and keep public APIs stable where possible.
|
||||||
|
- Document any breaking changes in the changelog and update AGENTS.md accordingly.
|
||||||
18
README.md
18
README.md
|
|
@ -1,3 +1,17 @@
|
||||||
# openenergysphere-federated-contract-driv
|
# OpenEnergySphere EnergiBridge (EnergiBridge MVP)
|
||||||
|
|
||||||
A novel, open-source platform that enables utilities, communities, and device vendors to collaboratively plan and optimize cross-domain energy systems (electricity, heating, water pumping) while preserving data privacy. It uses a graph-contract regis
|
This repository implements a production-ready skeleton for OpenEnergySphere's federated, contract-driven cross-domain energy planning platform. The MVP focuses on the EnergiBridge interoperability layer, mapping OpenEnergySphere primitives to a canonical IR and enabling plug-and-play adapters across DERs, meters, pumps, and buildings while preserving offline-first operation and governance.
|
||||||
|
|
||||||
|
Architecture highlights
|
||||||
|
- Core primitives: LocalProblem, SharedVariables, DualVariables, PlanDelta, PrivacyBudget, AuditLog
|
||||||
|
- Graph-of-Contracts registry for adapters and data schemas with per-message metadata
|
||||||
|
- Adapters: starter implementations (substation_meter, der_aggregator)
|
||||||
|
- EnergiBridge: mapping functions between OpenEnergySphere primitives and a CatOpt-like IR
|
||||||
|
- Server: FastAPI app exposing endpoints to register contracts and adapters
|
||||||
|
- Storage: in-memory by default, with potential for SQLite/PostgreSQL persistence
|
||||||
|
|
||||||
|
Getting started
|
||||||
|
- Install dependencies and run tests via test.sh
|
||||||
|
- Run API locally: uvicorn energysphere.server.main:app --reload
|
||||||
|
|
||||||
|
This README intentionally provides a compact architectural overview and a concrete MVP workflow to accelerate collaboration and iteration.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "openenergysphere-energibridge"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "EnergiBridge MVP: OpenEnergySphere federation core"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"fastapi>=0.90.0",
|
||||||
|
"uvicorn[standard]>=0.18.0",
|
||||||
|
"pydantic>=1.10.2",
|
||||||
|
"pytest>=7.0.0"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
package-dir = { "" = "src" }
|
||||||
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
""" Energysphere core package minimal init. """
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
from .substation_meter import SubstationMeterAdapter
|
||||||
|
from .der_aggregator import DERAggregatorAdapter
|
||||||
|
|
||||||
|
__all__ = ["SubstationMeterAdapter", "DERAggregatorAdapter"]
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from energysphere.models import LocalProblem
|
||||||
|
from energysphere.registry import GraphContractRegistry
|
||||||
|
|
||||||
|
|
||||||
|
class DERAggregatorAdapter:
|
||||||
|
adapter_id = "openenergysphere.adapter.der_aggregator"
|
||||||
|
|
||||||
|
def __init__(self, registry: GraphContractRegistry) -> None:
|
||||||
|
self._registry = registry
|
||||||
|
|
||||||
|
def collect_local_problem(self) -> LocalProblem:
|
||||||
|
lp = LocalProblem(
|
||||||
|
site_id="site-der-agg-001",
|
||||||
|
objective="min-carbon",
|
||||||
|
parameters={"der_count": 5, "time_horizon": 24},
|
||||||
|
)
|
||||||
|
self._registry.register_local_problem("contract-der-aggregator-001", lp)
|
||||||
|
return lp
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from energysphere.models import LocalProblem
|
||||||
|
from energysphere.registry import GraphContractRegistry
|
||||||
|
from energysphere.models import LocalProblem
|
||||||
|
|
||||||
|
|
||||||
|
class SubstationMeterAdapter:
|
||||||
|
"""Starter adapter: simulates a substation meter feeding a LocalProblem."""
|
||||||
|
|
||||||
|
adapter_id = "openenergysphere.adapter.substation_meter"
|
||||||
|
|
||||||
|
def __init__(self, registry: GraphContractRegistry) -> None:
|
||||||
|
self._registry = registry
|
||||||
|
|
||||||
|
def collect_local_problem(self) -> LocalProblem:
|
||||||
|
# Minimal sample LocalProblem; in a real system this would read from meters.
|
||||||
|
lp = LocalProblem(
|
||||||
|
site_id="site-meter-001",
|
||||||
|
objective="min-cost",
|
||||||
|
parameters={"load": 1.0, "time_horizon": 24},
|
||||||
|
)
|
||||||
|
# register a reference contract for this adapter's generated problem
|
||||||
|
self._registry.register_local_problem("contract-substation-meter-001", lp)
|
||||||
|
return lp
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class LocalProblem(BaseModel):
|
||||||
|
site_id: str
|
||||||
|
objective: str
|
||||||
|
parameters: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class SharedVariables(BaseModel):
|
||||||
|
name: str
|
||||||
|
value: Any
|
||||||
|
version: int
|
||||||
|
privacy_bound: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DualVariables(BaseModel):
|
||||||
|
primal: float
|
||||||
|
dual: float
|
||||||
|
|
||||||
|
|
||||||
|
class PlanDelta(BaseModel):
|
||||||
|
delta_id: str
|
||||||
|
changes: Dict[str, Any]
|
||||||
|
timestamp: float
|
||||||
|
|
||||||
|
|
||||||
|
class PrivacyBudget(BaseModel):
|
||||||
|
budget: float
|
||||||
|
used: float
|
||||||
|
|
||||||
|
|
||||||
|
class AuditLog(BaseModel):
|
||||||
|
entry_id: str
|
||||||
|
action: str
|
||||||
|
timestamp: float
|
||||||
|
actor: str
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from .models import LocalProblem
|
||||||
|
|
||||||
|
|
||||||
|
class GraphContractRegistry:
|
||||||
|
"""In-memory registry for Graph-of-Contracts.
|
||||||
|
|
||||||
|
This is a minimal skeleton intended for MVP testing and as a foundation for
|
||||||
|
a persistent registry backed by SQLite/PostgreSQL in the future.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._contracts: Dict[str, Dict[str, Any]] = {}
|
||||||
|
|
||||||
|
def register_contract(self, contract_id: str, contract_spec: Dict[str, Any]) -> None:
|
||||||
|
self._contracts[contract_id] = contract_spec
|
||||||
|
|
||||||
|
def list_contracts(self) -> List[Dict[str, Any]]:
|
||||||
|
return list(self._contracts.values())
|
||||||
|
|
||||||
|
def get_contract(self, contract_id: str) -> Dict[str, Any] | None:
|
||||||
|
return self._contracts.get(contract_id)
|
||||||
|
|
||||||
|
# Lightweight helper to store a LocalProblem as a contract reference
|
||||||
|
def register_local_problem(self, contract_id: str, lp: LocalProblem) -> None:
|
||||||
|
self.register_contract(contract_id, lp.dict())
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""EnergiBridge mapping helpers (skeleton).
|
||||||
|
|
||||||
|
Provides lightweight translation between OpenEnergySphere primitives and a
|
||||||
|
canonical IR (CatOpt-like) used by adapters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from .models import LocalProblem
|
||||||
|
|
||||||
|
|
||||||
|
class EnergiBridge:
|
||||||
|
@staticmethod
|
||||||
|
def map_local_problem_to_object(local_problem: LocalProblem) -> Dict[str, Any]:
|
||||||
|
"""Map a LocalProblem into a canonical IR object representation."""
|
||||||
|
return {
|
||||||
|
"type": "LocalProblem",
|
||||||
|
"site_id": local_problem.site_id,
|
||||||
|
"objective": local_problem.objective,
|
||||||
|
"parameters": local_problem.parameters,
|
||||||
|
"version": 1,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from energysphere.registry import GraphContractRegistry
|
||||||
|
from energysphere.models import LocalProblem
|
||||||
|
|
||||||
|
app = FastAPI(title="EnergiBridge MVP API")
|
||||||
|
|
||||||
|
# Simple in-memory stores
|
||||||
|
registry = GraphContractRegistry()
|
||||||
|
adapters_registry: dict[str, dict[str, str]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
class LocalProblemDTO(BaseModel):
|
||||||
|
site_id: str
|
||||||
|
objective: str
|
||||||
|
parameters: dict[str, object]
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/contracts/register")
|
||||||
|
def register_contract(lp: LocalProblemDTO):
|
||||||
|
problem = LocalProblem(**lp.dict())
|
||||||
|
contract_id = f"contract-{problem.site_id}"
|
||||||
|
registry.register_local_problem(contract_id, problem)
|
||||||
|
return {"contract_id": contract_id, "problem": problem.dict()}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/contracts")
|
||||||
|
def list_contracts():
|
||||||
|
return registry.list_contracts()
|
||||||
|
|
||||||
|
|
||||||
|
class AdapterDTO(BaseModel):
|
||||||
|
adapter_id: str
|
||||||
|
description: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/adapters/register")
|
||||||
|
def register_adapter(dto: AdapterDTO):
|
||||||
|
adapters_registry[dto.adapter_id] = {"description": dto.description or ""}
|
||||||
|
return {"registered": True, "adapter_id": dto.adapter_id}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
export PYTHONPATH=./src
|
||||||
|
echo "Installing dependencies..."
|
||||||
|
pip install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
|
||||||
|
pip install "pydantic>=1.10.2" "fastapi>=0.90.0" "uvicorn[standard]>=0.18.0" "pytest>=7.0.0" >/dev/null 2>&1 || true
|
||||||
|
echo "Running tests..."
|
||||||
|
pytest -q
|
||||||
|
echo "Running packaging build..."
|
||||||
|
python3 -m build
|
||||||
|
echo "All tests and build succeeded."
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
from energysphere.adapters.substation_meter import SubstationMeterAdapter
|
||||||
|
from energysphere.registry import GraphContractRegistry
|
||||||
|
from energysphere.models import LocalProblem
|
||||||
|
|
||||||
|
|
||||||
|
def test_substation_meter_adapter_generates_local_problem():
|
||||||
|
registry = GraphContractRegistry()
|
||||||
|
adapter = SubstationMeterAdapter(registry)
|
||||||
|
lp = adapter.collect_local_problem()
|
||||||
|
assert isinstance(lp, LocalProblem)
|
||||||
|
# The adapter should have registered the problem into the registry
|
||||||
|
contracts = registry.list_contracts()
|
||||||
|
assert any(c.get("site_id") == lp.site_id for c in contracts)
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from energysphere.registry import GraphContractRegistry
|
||||||
|
from energysphere.models import LocalProblem
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_register_and_list():
|
||||||
|
reg = GraphContractRegistry()
|
||||||
|
lp = LocalProblem(site_id="site-1", objective="min-cost", parameters={"load": 1.0})
|
||||||
|
reg.register_local_problem("contract-site-1", lp)
|
||||||
|
contracts = reg.list_contracts()
|
||||||
|
assert len(contracts) == 1
|
||||||
|
assert contracts[0]["site_id"] == "site-1"
|
||||||
Loading…
Reference in New Issue