from __future__ import annotations """ELAC-Plan API Simple FastAPI-based MVP to exercise LocalProblem -> PlanDelta flow with two starter adapters. This is intentionally lightweight and in-process to bootstrap cross-venue planning without external dependencies beyond FastAPI, Pydantic (via existing dataclasses), and httpx for tests. """ from datetime import datetime import json from typing import Dict, Any, Optional from fastapi import FastAPI, HTTPException from pydantic import BaseModel from .core import LocalProblem, PlanDelta, SharedVariables, DualVariables from .solver import LocalSolver from .adapters import NBBOFeedAdapter, BrokerGatewayAdapter from .dsl import ( DSLObject, DSLMorphisms, DSLDualVariables, DSLPlanDelta, to_dsl_object, to_dsl_morphisms, to_dsl_dual, to_dsl_plan, ) app = FastAPI(title="ELAC-Plan MVP API", version="0.1.0") # In-memory stores for MVP: problems and deltas _problems: Dict[str, LocalProblem] = {} _deltas: Dict[str, PlanDelta] = {} # Starter adapters (could be used by endpoints or in a factory pattern) _nbbo = NBBOFeedAdapter(contract_id="default-contract") _broker = BrokerGatewayAdapter() _solver = LocalSolver() class LocalProblemInput(BaseModel): id: str asset: str venue: str objective: str constraints: Optional[Dict[str, Any]] = None price_target: float tolerance: float @app.post("/problems", response_model=Dict[str, Any]) def create_problem(problem: LocalProblemInput): # Build a LocalProblem from input and run the toy solver to produce a delta lp = LocalProblem( id=problem.id, asset=problem.asset, venue=problem.venue, objective=problem.objective, constraints=problem.constraints or {}, price_target=problem.price_target, tolerance=problem.tolerance, ) _problems[lp.id] = lp delta = _solver.solve(lp) _deltas[delta.contract_id] = delta # Simulate publishing via broker gateway (stringized payload) for MVP _broker.publish(delta) return { "problem_id": lp.id, "delta": json.dumps(delta.delta), "delta_contract": delta.contract_id, } @app.get("/status") def status(): return { "problems_count": len(_problems), "deltas_count": len(_deltas), "latest_delta_contract": max(_deltas.keys()) if _deltas else None, "server_time": datetime.utcnow().isoformat() + "Z", } @app.get("/problems/{problem_id}") def get_problem(problem_id: str): lp = _problems.get(problem_id) if not lp: raise HTTPException(status_code=404, detail="problem not found") return { "id": lp.id, "asset": lp.asset, "venue": lp.venue, "objective": lp.objective, "price_target": lp.price_target, "tolerance": lp.tolerance, "constraints": lp.constraints, } @app.get("/deltas/{contract_id}") def get_delta(contract_id: str): delta = _deltas.get(contract_id) if not delta: raise HTTPException(status_code=404, detail="delta not found") return { "contract_id": delta.contract_id, "delta": delta.delta, "timestamp": delta.timestamp, "author": delta.author, "privacy_budget": delta.privacy_budget, } @app.get("/dsl/{problem_id}") def get_problem_dsl(problem_id: str): # Expose a minimal DSL representation for a given problem, including # LocalProblem -> DSLObject, SharedVariables, DualVariables, and PlanDelta. lp = _problems.get(problem_id) if not lp: raise HTTPException(status_code=404, detail="problem not found") # Build a deterministic contract_id as used by the MVP contract_id = f"{lp.id}:{lp.venue}" # Rehydrate a minimal SharedVariables and DualVariables for DSL exposure sw = SharedVariables(variables={}, version=1, contract_id=contract_id) dv = DualVariables(shadow_prices={}, version=1, contract_id=contract_id) # Resolve latest delta for the problem if available delta = _deltas.get(contract_id) if delta is None: # create a minimal placeholder delta to showcase DSL shape delta = PlanDelta( delta={}, timestamp="", author="unknown", contract_id=contract_id, privacy_budget=0.0, ) # Transform to DSL structures dsl_lp = to_dsl_object(lp) dsl_sw = to_dsl_morphisms(sw) dsl_dv = to_dsl_dual(dv) dsl_plan = to_dsl_plan(delta) return { "lp": dsl_lp.to_dict(), "shared_variables": dsl_sw.to_dict(), "dual_variables": dsl_dv.to_dict(), "plan_delta": dsl_plan.to_dict(), }