feat: Phase 0 walking skeleton — monorepo, API, web, ML stub
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>
This commit is contained in:
49
ml/serving/main.py
Normal file
49
ml/serving/main.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
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")
|
||||
9
ml/serving/package.json
Normal file
9
ml/serving/package.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@oo/ml-serving",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "uvicorn main:app --reload --port 8000",
|
||||
"start": "uvicorn main:app --port 8000"
|
||||
}
|
||||
}
|
||||
3
ml/serving/requirements.txt
Normal file
3
ml/serving/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
fastapi==0.115.6
|
||||
uvicorn[standard]==0.32.1
|
||||
pydantic==2.10.4
|
||||
Reference in New Issue
Block a user