feat(ml): prompt registry + per-request variant selection
Replaces the hardcoded "v1" label with a real prompt registry:
ml/serving/prompts.py — keyed by version: v1 (baseline),
v2-mentor (calm/specific persona),
v3-few-shot (v1 persona + curated examples)
ml/serving/main.py — POST /generate accepts optional prompt_version,
422 on unknown, echoes the version actually used
back in the response
services/api/src/config.ts — TIP_PROMPT_VERSION: empty / single / comma-list
(uniform random per request)
services/api/src/routes/recommender.ts
— pickPromptVersion() drives selection; the
response's prompt_version (not a stale TS
constant) is what lands in tip_scores so the
#92 reward-analytics dashboard shows real
per-variant reaction rates
Closes #84.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,8 @@ import numpy as np
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from prompts import get_prompt
|
||||
|
||||
app = FastAPI(title="oO ML Serving", version="1.0.0")
|
||||
|
||||
LITELLM_URL = os.getenv("LITELLM_URL", "http://localhost:4000")
|
||||
@@ -181,6 +183,7 @@ class GenerateRequest(BaseModel):
|
||||
user_id: str
|
||||
context: PromptContext = PromptContext()
|
||||
n: int = 3
|
||||
prompt_version: Optional[str] = None # None → server default (env DEFAULT_PROMPT_VERSION)
|
||||
|
||||
|
||||
class TipCandidate(BaseModel):
|
||||
@@ -193,33 +196,11 @@ class TipCandidate(BaseModel):
|
||||
class GenerateResponse(BaseModel):
|
||||
candidates: list[TipCandidate]
|
||||
model: str
|
||||
prompt_version: str
|
||||
prompt_tokens: int = 0
|
||||
completion_tokens: int = 0
|
||||
|
||||
|
||||
_GENERATE_SYSTEM = (
|
||||
"You are a personal productivity coach. "
|
||||
"Given the user's current context, generate actionable, specific tips. "
|
||||
"Respond ONLY with a JSON array of objects, each with keys: "
|
||||
'"id" (short slug), "content" (the tip, ≤2 sentences), "rationale" (why now, ≤1 sentence). '
|
||||
"No markdown, no prose outside the JSON array."
|
||||
)
|
||||
|
||||
|
||||
def _build_prompt(ctx: PromptContext, n: int) -> str:
|
||||
lines = [f"Time: {ctx.hour_of_day:02d}:00, day_of_week={ctx.day_of_week}"]
|
||||
if ctx.tasks:
|
||||
overdue = [t for t in ctx.tasks if t.get("is_overdue")]
|
||||
lines.append(f"Tasks: {len(ctx.tasks)} total, {len(overdue)} overdue")
|
||||
for t in ctx.tasks[:5]:
|
||||
due = t.get("due_date", "no due date")
|
||||
lines.append(f" - [{t.get('priority','?')}] {t.get('content','?')} (due: {due})")
|
||||
for k, v in ctx.extra.items():
|
||||
lines.append(f"{k}: {v}")
|
||||
lines.append(f"\nGenerate {n} tips as a JSON array.")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ── Endpoints ──────────────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/health")
|
||||
@@ -253,10 +234,14 @@ async def generate(req: GenerateRequest) -> GenerateResponse:
|
||||
Retries up to _MAX_GENERATE_RETRIES times on malformed JSON, appending
|
||||
a correction hint to the conversation so the model can self-correct.
|
||||
"""
|
||||
prompt = _build_prompt(req.context, req.n)
|
||||
try:
|
||||
prompt_template = get_prompt(req.prompt_version)
|
||||
except KeyError as e:
|
||||
raise HTTPException(status_code=422, detail=f"Unknown prompt_version: {e.args[0]}")
|
||||
user_msg = prompt_template.build_user(req.context, req.n)
|
||||
messages: list[dict] = [
|
||||
{"role": "system", "content": _GENERATE_SYSTEM},
|
||||
{"role": "user", "content": prompt},
|
||||
{"role": "system", "content": prompt_template.system},
|
||||
{"role": "user", "content": user_msg},
|
||||
]
|
||||
headers = {"Authorization": f"Bearer {LITELLM_MASTER_KEY}"}
|
||||
last_parse_error: str = ""
|
||||
@@ -313,6 +298,7 @@ async def generate(req: GenerateRequest) -> GenerateResponse:
|
||||
return GenerateResponse(
|
||||
candidates=candidates,
|
||||
model=model_used,
|
||||
prompt_version=prompt_template.version,
|
||||
prompt_tokens=total_usage["prompt_tokens"],
|
||||
completion_tokens=total_usage["completion_tokens"],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user