170 lines
5.9 KiB
Python
170 lines
5.9 KiB
Python
from fastapi import FastAPI, Request, HTTPException
|
|
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
|
from jinja2 import Environment, FileSystemLoader
|
|
import httpx, os, json
|
|
from k8s import create_service, delete_service, list_user_services
|
|
from minio_client import ensure_bucket
|
|
|
|
app = FastAPI()
|
|
jinja_env = Environment(loader=FileSystemLoader("/app/code/templates"))
|
|
|
|
KEYCLOAK_URL = "https://keycloak.lab.leadwire.dev/auth/realms/datalab"
|
|
CLIENT_ID = "datalab-lite"
|
|
CLIENT_SECRET= os.getenv("CLIENT_SECRET", "")
|
|
REDIRECT_URI = "https://datalablite.lab.leadwire.dev/callback"
|
|
|
|
SERVICES = {
|
|
"jupyter": {
|
|
"name": "JupyterLab",
|
|
"image": "jupyter/scipy-notebook:latest",
|
|
"port": 8888,
|
|
"icon": "🔬",
|
|
"env": {"JUPYTER_ENABLE_LAB": "yes", "JUPYTER_TOKEN": "{token}"}
|
|
},
|
|
"rstudio": {
|
|
"name": "RStudio",
|
|
"image": "rocker/rstudio:latest",
|
|
"port": 8787,
|
|
"icon": "📊",
|
|
"env": {"PASSWORD": "{token}", "USER": "rstudio"}
|
|
},
|
|
"vscode": {
|
|
"name": "VSCode Server",
|
|
"image": "codercom/code-server:latest",
|
|
"port": 8080,
|
|
"icon": "💻",
|
|
"env": {"PASSWORD": "{token}"}
|
|
}
|
|
}
|
|
|
|
def render(template_name, **ctx):
|
|
t = jinja_env.get_template(template_name)
|
|
return HTMLResponse(t.render(**ctx))
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def home(request: Request):
|
|
username = request.cookies.get("username")
|
|
if not username:
|
|
return RedirectResponse("/login")
|
|
services = list_user_services(username)
|
|
return render("index.html", username=username, services=services, catalog=SERVICES)
|
|
|
|
@app.get("/login")
|
|
async def login():
|
|
url = (
|
|
f"{KEYCLOAK_URL}/protocol/openid-connect/auth"
|
|
f"?client_id={CLIENT_ID}"
|
|
f"&redirect_uri={REDIRECT_URI}"
|
|
f"&response_type=code"
|
|
f"&scope=openid profile email"
|
|
)
|
|
return RedirectResponse(url)
|
|
|
|
@app.get("/callback")
|
|
async def callback(code: str, request: Request):
|
|
async with httpx.AsyncClient(verify=False) as client:
|
|
resp = await client.post(
|
|
f"{KEYCLOAK_URL}/protocol/openid-connect/token",
|
|
data={
|
|
"grant_type": "authorization_code",
|
|
"client_id": CLIENT_ID,
|
|
"client_secret": CLIENT_SECRET,
|
|
"code": code,
|
|
"redirect_uri": REDIRECT_URI,
|
|
}
|
|
)
|
|
if resp.status_code != 200:
|
|
raise HTTPException(status_code=401, detail="Auth failed")
|
|
|
|
tokens = resp.json()
|
|
access_token = tokens["access_token"]
|
|
|
|
import base64
|
|
payload = access_token.split(".")[1]
|
|
payload += "=" * (4 - len(payload) % 4)
|
|
user_info = json.loads(base64.b64decode(payload))
|
|
username = user_info.get("preferred_username", "unknown")
|
|
|
|
ensure_bucket(username)
|
|
|
|
response = RedirectResponse("/", status_code=302)
|
|
response.set_cookie("token", access_token, httponly=True)
|
|
response.set_cookie("username", username, httponly=True)
|
|
return response
|
|
|
|
@app.get("/logout")
|
|
async def logout():
|
|
response = RedirectResponse("/login")
|
|
response.delete_cookie("token")
|
|
response.delete_cookie("username")
|
|
return response
|
|
|
|
@app.post("/api/launch/{service_type}")
|
|
async def launch(service_type: str, request: Request):
|
|
username = request.cookies.get("username")
|
|
if not username:
|
|
raise HTTPException(status_code=401)
|
|
if service_type not in SERVICES:
|
|
raise HTTPException(status_code=400, detail="Service inconnu")
|
|
svc = SERVICES[service_type]
|
|
url = create_service(username, service_type, svc)
|
|
return JSONResponse({"status": "ok", "url": url})
|
|
|
|
@app.delete("/api/delete/{service_type}")
|
|
async def delete(service_type: str, request: Request):
|
|
username = request.cookies.get("username")
|
|
if not username:
|
|
raise HTTPException(status_code=401)
|
|
delete_service(username, service_type)
|
|
return JSONResponse({"status": "deleted"})
|
|
|
|
@app.get("/api/services")
|
|
async def services(request: Request):
|
|
username = request.cookies.get("username")
|
|
if not username:
|
|
raise HTTPException(status_code=401)
|
|
return JSONResponse(list_user_services(username))
|
|
|
|
@app.get("/api/minio-credentials")
|
|
async def minio_credentials(request: Request):
|
|
username = request.cookies.get("username")
|
|
if not username:
|
|
raise HTTPException(status_code=401)
|
|
from minio_client import get_minio_credentials
|
|
return JSONResponse(get_minio_credentials(username))
|
|
|
|
@app.get("/api/minio-credentials")
|
|
async def minio_credentials(request: Request):
|
|
username = request.cookies.get("username")
|
|
if not username:
|
|
raise HTTPException(status_code=401)
|
|
from minio_client import get_minio_credentials
|
|
return JSONResponse(get_minio_credentials(username))
|
|
|
|
@app.post("/api/chat")
|
|
async def chat(request: Request):
|
|
username = request.cookies.get("username")
|
|
if not username:
|
|
raise HTTPException(status_code=401)
|
|
body = await request.json()
|
|
messages = body.get("messages", [])
|
|
async with httpx.AsyncClient(verify=False, timeout=60) as client:
|
|
resp = await client.post(
|
|
"http://10.233.9.253:4000/v1/chat/completions",
|
|
headers={"Authorization": "Bearer sk-local123test",
|
|
"Content-Type": "application/json"},
|
|
json={
|
|
"model": "llama3.2:3b",
|
|
"messages": [
|
|
{"role": "system", "content": f"Tu es un assistant expert Data Science pour {username}. Tu aides avec Python, pandas, SQL, machine learning, et l'utilisation de MinIO S3. Réponds en français de manière concise."}
|
|
] + messages,
|
|
"temperature": 0.7,
|
|
"max_tokens": 512,
|
|
"stream": False
|
|
}
|
|
)
|
|
if resp.status_code != 200:
|
|
raise HTTPException(status_code=502, detail="LiteLLM error")
|
|
result = resp.json()
|
|
return JSONResponse({"response": result["choices"][0]["message"]["content"]})
|