ring-sizer / src /ai_recommendation.py
feng-x's picture
Upload folder using huggingface_hub
d6218c1 verified
"""AI-powered ring size explanation using OpenAI.
Size selection is handled by deterministic logic in ring_size.py.
This module only generates a human-friendly explanation for the recommendation.
"""
import os
import logging
from typing import Dict, Optional
from src.ring_size import RING_MODELS, DEFAULT_RING_MODEL
logger = logging.getLogger(__name__)
_MODEL_LABELS = {"gen": "Gen1/Gen2", "air": "Air"}
def _build_size_table_text(ring_model: str = DEFAULT_RING_MODEL) -> str:
chart = RING_MODELS.get(ring_model, RING_MODELS[DEFAULT_RING_MODEL])
return "\n".join(
f" Size {size}: inner diameter {diameter_mm:.1f} mm"
for size, diameter_mm in sorted(chart.items())
)
_SYSTEM_PROMPT_TEMPLATE = """You are a sizing explanation assistant for Femometer Smart Ring ({model_label}).
You are given measured finger widths and a pre-computed ring size recommendation.
Your ONLY job is to explain WHY the recommended size is a good fit, in 1-2 concise sentences.
Do NOT suggest a different size. The size decision has already been made by the system.
Guidelines:
- Mention which finger(s) would fit best at this size
- Include specific diameter when first referencing a size, e.g. "Size 8 (18.6mm)"
- Priority context: index finger fit is slightly preferred over middle, then ring
- Keep it concise and actionable
Ring Size Chart ({model_label}):
{size_table}
Respond in plain text (1-2 sentences). Do NOT use JSON or markdown.
"""
def ai_explain_recommendation(
finger_widths: Dict[str, Optional[float]],
recommended_size: int,
range_min: int,
range_max: int,
ring_model: str = DEFAULT_RING_MODEL,
) -> Optional[str]:
"""Call OpenAI to explain an already-computed ring size recommendation.
Args:
finger_widths: Dict mapping finger name to diameter in cm (or None if failed).
Example: {"index": 1.93, "middle": 1.84, "ring": 1.93}
recommended_size: The deterministic best-match size from ring_size.py.
range_min: Lower bound of recommended size range.
range_max: Upper bound of recommended size range.
ring_model: Which ring model chart to reference.
Returns:
A plain-text explanation string, or None if the API call fails.
"""
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
logger.warning("OPENAI_API_KEY not set, skipping AI explanation")
return None
model_label = _MODEL_LABELS.get(ring_model, ring_model)
system_prompt = _SYSTEM_PROMPT_TEMPLATE.format(
model_label=model_label,
size_table=_build_size_table_text(ring_model),
)
# Build user message with measurements and pre-computed recommendation
lines = ["Measured finger outer diameters:"]
for finger, width in finger_widths.items():
if width is not None:
lines.append(f" {finger.capitalize()}: {width:.2f} cm ({width * 10:.1f} mm)")
else:
lines.append(f" {finger.capitalize()}: measurement failed")
lines.append("")
lines.append(f"Recommended size: {recommended_size} (range {range_min}\u2013{range_max})")
lines.append("")
lines.append("Explain why this size is a good fit.")
user_msg = "\n".join(lines)
try:
import openai
client = openai.OpenAI(api_key=api_key)
response = client.chat.completions.create(
model="gpt-5.4",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_msg},
],
temperature=0.3,
max_completion_tokens=200,
)
content = response.choices[0].message.content.strip()
if not content:
logger.warning("AI returned empty explanation")
return None
return content
except Exception as e:
logger.error("AI explanation failed: %s", e)
return None