""" IOM API - FastAPI application for Infrastructure Object Model Combines all layers into a unified API """ from fastapi import FastAPI, HTTPException, Query, Depends, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.responses import JSONResponse from typing import List, Optional, Dict from datetime import datetime, date import time app = FastAPI( title="IOM API", description="Infrastructure Object Model API", version="1.0.0", dependencies=[Depends(HTTPBearer(auto_error=False))] ) # CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) # Import modules import sys sys.path.insert(0, '/home/bernt/.openclaw/workspace/iom/core') sys.path.insert(0, '/home/bernt/.openclaw/workspace/iom/observation') sys.path.insert(0, '/home/bernt/.openclaw/workspace/iom/defect') sys.path.insert(0, '/home/bernt/.openclaw/workspace/iom/risk') sys.path.insert(0, '/home/bernt/.openclaw/workspace/iom/ledger') sys.path.insert(0, '/home/bernt/.openclaw/workspace/iom/visual_geolocation') from goid_generator import GOIDGenerator, validate_goid, parse_goid from observation_models import Observation, ObservationSummary from defect_codes import DefectRegistry, DefectCategory from risk_model import RiskCalculator, RiskScores from iom_ledger_mapping import IOMLedgerMapper from api.auth import auth_manager, UserRole, Permission # Visual Geolocation from visual_geolocation.pipeline import VisualGeolocationPipeline from visual_geolocation.temporal_analysis import TemporalAnalyzer # Initialize components goid_gen = GOIDGenerator() defect_reg = DefectRegistry() risk_calc = RiskCalculator() ledger_mapper = IOMLedgerMapper() visual_geo = VisualGeolocationPipeline(use_real_ai=True) temporal_analyzer = TemporalAnalyzer() # Auth helper def require_auth(): return Depends(auth_manager.get_current_user) def require_permission(permission: Permission): return Depends(auth_manager.require_permission(permission)) # === Auth Endpoints === class AuthRequest: def __init__(self, username: str, password: str, role: str = "readonly"): self.username = username self.password = password self.role = role @app.post("/auth/register") async def register_user(request: Dict): """Register a new user""" username = request.get("username") password = request.get("password") role = request.get("role", "readonly") if not username or not password: raise HTTPException(status_code=400, detail="Username and password required") try: user_role = UserRole(role) except ValueError: raise HTTPException(status_code=400, detail=f"Invalid role: {role}") # In production, hash password and store in database token = auth_manager.create_token(username, user_role) return {"token": token, "role": role} @app.post("/auth/login") async def login(request: Dict): """Login and get token""" username = request.get("username") password = request.get("password") if not username or not password: raise HTTPException(status_code=400, detail="Username and password required") # In production, verify password against database token = auth_manager.create_token(username, UserRole.READONLY) return {"token": token} # === GOID Endpoints === @app.get("/goid/validate/{goid}") async def validate_goid_endpoint(goid: str, user: Dict = Depends(auth_manager.get_current_user)): """Validate a GOID""" is_valid, error = validate_goid(goid) return {"valid": is_valid, "error": error} @app.get("/goid/parse/{goid}") async def parse_goid_endpoint(goid: str, user: Dict = Depends(auth_manager.get_current_user)): """Parse a GOID into components""" try: parsed = parse_goid(goid) return parsed except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/goid/generate") async def generate_goid( domain: str, system: str, subsystem: str, obj_type: str, location_hash: Optional[str] = None, user: Dict = Depends(auth_manager.get_current_user) ): """Generate a new GOID""" try: goid = goid_gen.generate(domain, system, subsystem, obj_type, location_hash) return {"goid": goid} except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @app.get("/taxonomy/domains") async def list_domains(user: Dict = Depends(auth_manager.get_current_user)): """List all domains""" return goid_gen.list_domains() @app.get("/taxonomy/domains/{domain}/systems") async def list_systems(domain: str, user: Dict = Depends(auth_manager.get_current_user)): """List systems in a domain""" try: return goid_gen.list_systems(domain) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) @app.get("/taxonomy/domains/{domain}/systems/{system}/objects") async def list_objects(domain: str, system: str, user: Dict = Depends(auth_manager.get_current_user)): """List objects in a system""" try: return goid_gen.list_objects(domain, system) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) # === Observation Endpoints === @app.post("/observations") async def create_observation(observation: Observation, user: Dict = Depends(auth_manager.get_current_user)): """Create a new observation""" # In production, this would save to database return { "id": observation.id or f"OBS-{datetime.now().year}-0000001", "status": "created", "object_goid": observation.object_goid } @app.get("/observations/{observation_id}") async def get_observation(observation_id: str, user: Dict = Depends(auth_manager.get_current_user)): """Get observation by ID""" # In production, this would fetch from database raise HTTPException(status_code=404, detail="Observation not found") @app.get("/objects/{goid}/observations") async def get_object_observations( goid: str, limit: int = Query(100, ge=1, le=1000), offset: int = Query(0, ge=0), user: Dict = Depends(auth_manager.get_current_user) ): """Get observations for an object""" # In production, this would fetch from database return { "object_goid": goid, "observations": [], "total": 0 } @app.get("/objects/{goid}/summary") async def get_object_summary(goid: str, user: Dict = Depends(auth_manager.get_current_user)): """Get observation summary for an object""" # In production, this would fetch from database return { "object_goid": goid, "observation_count": 0, "latest_condition": None } # === Defect Endpoints === @app.get("/defects") async def list_defects( category: Optional[str] = None, lang: str = "sv", user: Dict = Depends(auth_manager.get_current_user) ): """List all defect codes""" if category: try: cat = DefectCategory(category) codes = defect_reg.get_by_category(cat) except ValueError: raise HTTPException(status_code=400, detail=f"Invalid category: {category}") else: codes = list(defect_reg._codes.values()) return [code.to_dict(lang) for code in codes] @app.get("/defects/{code}") async def get_defect(code: str, lang: str = "sv", user: Dict = Depends(auth_manager.get_current_user)): """Get defect code by code""" defect = defect_reg.get(code) if not defect: raise HTTPException(status_code=404, detail=f"Defect code not found: {code}") return defect.to_dict(lang) @app.get("/defects/search/{query}") async def search_defects(query: str, lang: str = "sv", user: Dict = Depends(auth_manager.get_current_user)): """Search defect codes""" results = defect_reg.search(query, lang) return [code.to_dict(lang) for code in results] @app.post("/defects/suggest") async def suggest_defects(keywords: List[str], user: Dict = Depends(auth_manager.get_current_user)): """Suggest defect codes based on AI keywords""" suggestions = defect_reg.get_ai_suggestions(keywords) return [ { "code": code.code, "name": code.name_en, "confidence": confidence } for code, confidence in suggestions ] # === Risk Endpoints === @app.post("/risk/calculate") async def calculate_risk( scores: RiskScores, object_type: str = "default", user: Dict = Depends(auth_manager.get_current_user) ): """Calculate risk score""" result = risk_calc.calculate(scores, object_type) return result @app.post("/risk/calculate-from-observation") async def calculate_risk_from_observation( condition: int, defect_codes: List[str], object_type: str = "default", user: Dict = Depends(auth_manager.get_current_user) ): """Calculate risk from observation data""" result = risk_calc.calculate_from_observation(condition, defect_codes, object_type) return result @app.get("/risk/weights/{object_type}") async def get_risk_weights(object_type: str, user: Dict = Depends(auth_manager.get_current_user)): """Get risk weights for object type""" weights = risk_calc.get_weights_for_type(object_type) return {"object_type": object_type, "weights": weights} # === Ledger Endpoints === @app.post("/ledger/installation") async def map_installation( goid: str, object_type: str, cost: float, installation_date: date, contractor: str = "", project: str = "", user: Dict = Depends(auth_manager.get_current_user) ): """Map installation to ledger transactions""" transactions = ledger_mapper.map_installation( goid, object_type, cost, installation_date, contractor, project ) return { "goid": goid, "transactions": [t.to_dict() for t in transactions] } @app.post("/ledger/maintenance") async def map_maintenance( goid: str, cost: float, maintenance_date: date, observation_id: str = "", defect_code: str = "", contractor: str = "", description: str = "", user: Dict = Depends(auth_manager.get_current_user) ): """Map maintenance to ledger transactions""" transactions = ledger_mapper.map_maintenance( goid, cost, maintenance_date, observation_id, defect_code, contractor, description ) return { "goid": goid, "transactions": [t.to_dict() for t in transactions] } @app.post("/ledger/depreciation") async def map_depreciation( goid: str, object_type: str, acquisition_value: float, useful_life: int, depreciation_date: date, user: Dict = Depends(auth_manager.get_current_user) ): """Map depreciation to ledger transactions""" transactions = ledger_mapper.map_depreciation( goid, object_type, acquisition_value, useful_life, depreciation_date ) return { "goid": goid, "transactions": [t.to_dict() for t in transactions] } # === Visual Geolocation Endpoints === @app.post("/visual-geolocation/analyze") async def analyze_image(request: Dict): """Analyze image and extract evidence package""" image_path = request.get("image_path") image_id = request.get("image_id") if not image_path: raise HTTPException(status_code=400, detail="image_path required") try: result = visual_geo.process_image(image_path, image_id) return { "image_id": image_id or "unknown", "position": { "lat": result.lat, "lng": result.lng, "accuracy": result.accuracy, "confidence": result.confidence }, "method": result.method, "evidence_summary": result.evidence_summary, "confidence_report": { "overall": result.confidence_report.overall_confidence, "uncertainty_radius": result.confidence_report.uncertainty_radius, "supporting_evidence_count": len(result.confidence_report.supporting_evidence), "contradicting_evidence_count": len(result.confidence_report.contradicting_evidence) }, "map_matches": result.map_matches, "similar_images": result.similar_images } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/visual-geolocation/batch") async def analyze_batch(request: Dict): """Analyze multiple images""" images = request.get("images", []) if not images: raise HTTPException(status_code=400, detail="images array required") results = [] for img in images: try: result = visual_geo.process_image(img.get("path"), img.get("id")) results.append({ "image_id": img.get("id"), "position": { "lat": result.lat, "lng": result.lng, "accuracy": result.accuracy }, "confidence": result.confidence }) except Exception as e: results.append({ "image_id": img.get("id"), "error": str(e) }) return {"results": results} @app.post("/visual-geolocation/temporal") async def temporal_analysis(request: Dict): """Analyze temporal changes between observations""" observations = request.get("observations", []) if len(observations) < 2: raise HTTPException(status_code=400, detail="At least 2 observations required") # Add observations to analyzer for obs in observations: # In production, load from database # For now, create from request pass # Compare consecutive observations comparisons = [] for i in range(len(observations) - 1): # In production, use actual evidence packages comparisons.append({ "from": observations[i].get("id"), "to": observations[i + 1].get("id"), "status": "compared" }) return { "comparisons": len(comparisons), "results": comparisons } # === Health Check === @app.get("/health") async def health_check(): """Health check endpoint""" return { "status": "healthy", "version": "1.0.0", "components": { "goid": "ok", "taxonomy": "ok", "defects": "ok", "risk": "ok", "ledger": "ok", "auth": "ok", "visual_geolocation": "ok" } } if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)