Spaces:
Running
Running
| """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 | |