Skip to content
Draft
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
64 changes: 64 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Dependencies
node_modules/
__pycache__/
*.pyc
*.pyo
.venv/
venv/
env/

# Build outputs
dist/
build/
artifacts/
cache/
typechain/
typechain-types/

# Environment files
.env
.env.local
.env.*.local

# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
logs/

# OS
.DS_Store
Thumbs.db

# IDE
.vscode/
.idea/
*.swp
*.swo

# Coverage
coverage/
.nyc_output/
htmlcov/

# Hardhat
blockchain/artifacts/
blockchain/cache/
blockchain/ignition/deployments/

# Frontend
frontend/dist/

# Python
*.egg-info/
dist/
.pytest_cache/

# Docker
.dockerignore

# Misc
*.zip
*.tar.gz
deployments.json
3 changes: 3 additions & 0 deletions ai-service/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# AI Service Environment Variables
PORT=8000
REPORTS_DIR=/tmp/faltric_reports
7 changes: 7 additions & 0 deletions ai-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
122 changes: 122 additions & 0 deletions ai-service/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import os
import uuid
from pathlib import Path
from typing import List, Optional

from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

from models.recommendation import RecommendationEngine

load_dotenv()

app = FastAPI(
title="Faltric AI Service",
description="Energy source recommendation and prediction API",
version="1.0.0",
)

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)

REPORTS_DIR = Path(os.getenv("REPORTS_DIR", "/tmp/faltric_reports"))
REPORTS_DIR.mkdir(parents=True, exist_ok=True)

engine = RecommendationEngine()


class HistoricalEntry(BaseModel):
generation: float
consumption: float
date: Optional[str] = None


class WeatherData(BaseModel):
solar_irradiance: float = 500.0 # W/m²
wind_speed: float = 5.0 # m/s
precipitation: float = 0.0 # mm


class PredictRequest(BaseModel):
installation_id: str
installation_type: Optional[str] = None
historical_data: List[HistoricalEntry] = []
weather: WeatherData = WeatherData()


class PredictResponse(BaseModel):
recommended_source: str
score: float
estimated_earnings: float
report_url: Optional[str] = None


@app.get("/health")
def health():
return {"status": "ok"}


@app.post("/predict", response_model=PredictResponse)
def predict(req: PredictRequest):
try:
historical = [
{"generation": e.generation, "consumption": e.consumption}
for e in req.historical_data
]
weather = {
"solar_irradiance": req.weather.solar_irradiance,
"wind_speed": req.weather.wind_speed,
"precipitation": req.weather.precipitation,
}

result = engine.recommend(
installation_id=req.installation_id,
installation_type=req.installation_type,
historical_data=historical,
weather=weather,
)

# Generate PDF report
report_id = str(uuid.uuid4())
report_path = REPORTS_DIR / f"report_{report_id}.pdf"
engine.generate_report(
path=str(report_path),
installation_id=req.installation_id,
result=result,
weather=weather,
)

result["report_url"] = f"/reports/{report_id}"
return result

except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@app.get("/reports/{report_id}")
def get_report(report_id: str):
# Validate proper UUID format using Python's uuid module
try:
uuid.UUID(report_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid report ID format")
path = (REPORTS_DIR / f"report_{report_id}.pdf").resolve()
# Guard against path traversal: ensure the resolved path stays within REPORTS_DIR
if not str(path).startswith(str(REPORTS_DIR.resolve())):
raise HTTPException(status_code=400, detail="Invalid report ID format")
if not path.exists():
raise HTTPException(status_code=404, detail="Report not found")
return FileResponse(path, media_type="application/pdf", filename=f"faltric_report_{report_id}.pdf")


if __name__ == "__main__":
import uvicorn
port = int(os.getenv("PORT", 8000))
uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True)
Loading