Sets up the full Phase 0 foundation: - pnpm workspaces + turbo build graph; native module build approval - packages/shared-types: HTTP contracts (Tip, Auth, Integrations, User) - services/api: Express modular monolith with better-sqlite3/drizzle - auth: Google OAuth2 + PKCE via openid-client v6, cookie sessions - integrations: Todoist OAuth2 connect/disconnect, token vault - recommender: RandomPolicy over Todoist tasks, feedback sink - user: profile, consent capture, full account deletion (GDPR) - apps/web: Next.js 15, three pages (sign-in → connect → tip) - tip page: black canvas, hold-to-act gesture, action sheet - PWA manifest + theme - ml/serving: FastAPI stub implementing the POST /score contract - infra: docker-compose (core/full profiles), Dockerfiles, CI skeleton - .env.example with all required vars documented Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
50 lines
1.2 KiB
Python
50 lines
1.2 KiB
Python
"""
|
|
oO ML Serving — Phase 0 stub.
|
|
|
|
Returns a placeholder response that matches the interface the real scorer will implement.
|
|
The recommender service calls this via RemotePolicy (not yet wired in Phase 0).
|
|
|
|
Contract:
|
|
POST /score
|
|
Body: { user_id: str, candidates: [{ id: str, content: str, source: str, source_id?: str }] }
|
|
Response: { tip_id: str, score: float }
|
|
"""
|
|
|
|
from fastapi import FastAPI, HTTPException
|
|
from pydantic import BaseModel
|
|
import random
|
|
|
|
app = FastAPI(title="oO ML Serving", version="0.0.0")
|
|
|
|
|
|
class Candidate(BaseModel):
|
|
id: str
|
|
content: str
|
|
source: str
|
|
source_id: str | None = None
|
|
|
|
|
|
class ScoreRequest(BaseModel):
|
|
user_id: str
|
|
candidates: list[Candidate]
|
|
|
|
|
|
class ScoreResponse(BaseModel):
|
|
tip_id: str
|
|
score: float
|
|
policy: str
|
|
|
|
|
|
@app.get("/health")
|
|
def health():
|
|
return {"ok": True}
|
|
|
|
|
|
@app.post("/score", response_model=ScoreResponse)
|
|
def score(req: ScoreRequest):
|
|
if not req.candidates:
|
|
raise HTTPException(status_code=422, detail="No candidates")
|
|
# Stub: random uniform scoring — real model slots in here
|
|
chosen = random.choice(req.candidates)
|
|
return ScoreResponse(tip_id=chosen.id, score=1.0, policy="stub-random")
|