match.py
Ricky/cat_project/backend/app/routes/match.py
import json
import logging
from flask import Blueprint, jsonify, request, current_app
from ..services.storage import Storage
from ..services.poe_analyze import (
analyze_with_poe,
generate_kitten_image
)
from ..utils.auth import require_jwt
from ..utils.media_url import resolve_stored_media_url
logger = logging.getLogger(__name__)
def _cat_for_api(cat: dict) -> dict:
if not cat:
return cat
out = dict(cat)
out["photo_path"] = resolve_stored_media_url(current_app.config, out.get("photo_path"))
return out
def _match_item_for_api(item: dict) -> dict:
out = dict(item)
c = out.get("cat")
if isinstance(c, dict):
out["cat"] = _cat_for_api(c)
return out
bp = Blueprint("match", __name__, url_prefix="/api")
# ----------------------------
# Matching score logic
# ----------------------------
def _different_gender(my_gender: str, other_gender: str) -> bool:
"""Return True only if both genders are set and different (case-insensitive)."""
a = (my_gender or "").strip().lower()
b = (other_gender or "").strip().lower()
if not a or not b:
return False
return a != b
def _score(my_cat: dict, other: dict) -> dict:
score = 0
# 1. Same breed (mild preference only; cross-breed matching is allowed)
if (my_cat.get("breed") or "") == (other.get("breed") or ""):
score += 8
# 2. Age similarity
try:
age_diff = abs(int(my_cat.get("age", 0)) - int(other.get("age", 0)))
score += max(0, 25 - min(25, age_diff * 5))
except Exception:
pass
# 3. Vaccination overlap
my_v = set(my_cat.get("vaccinations") or [])
ot_v = set(other.get("vaccinations") or [])
score += min(10, len(my_v.intersection(ot_v)) * 2)
# 4. Health similarity
if (my_cat.get("health") or "").lower() == (other.get("health") or "").lower():
score += 10
return {
"cat": other,
"score": int(score),
}
def _norm(v: str) -> str:
return (v or "").strip().lower()
def _select_diverse_matches(scored: list, my_breed: str, limit: int = 5) -> list:
"""
Ensure cross-breed candidates are included when available.
Strategy:
- keep score ranking as baseline
- reserve up to 2 slots for cross-breed cats (if available)
"""
if not scored:
return []
mine = _norm(my_breed)
cross = [x for x in scored if _norm((x.get("cat") or {}).get("breed")) != mine]
same = [x for x in scored if _norm((x.get("cat") or {}).get("breed")) == mine]
reserve_cross = min(2, len(cross), limit)
chosen = cross[:reserve_cross]
used_ids = {
(item.get("cat") or {}).get("id")
for item in chosen
if (item.get("cat") or {}).get("id") is not None
}
for item in scored:
cat_id = (item.get("cat") or {}).get("id")
if cat_id is not None and cat_id in used_ids:
continue
chosen.append(item)
if len(chosen) >= limit:
break
return chosen[:limit]
# ----------------------------
# Prompt builder (Poe analyze)
# ----------------------------
def _build_prompt(my_cat: dict, top_matches: list) -> str:
my_profile = my_cat.get("profile") or {}
my_summary = {
"name": my_cat.get("name"),
"breed": my_cat.get("breed"),
"age": my_cat.get("age"),
"sex_status": my_cat.get("gender"),
"health": my_cat.get("health"),
"vaccinations": my_cat.get("vaccinations"),
"source": my_profile.get("source"),
"owner_experience_years": my_profile.get("experience_years"),
"origin_knowledge": my_profile.get("origin_knowledge"),
}
candidates = []
for item in top_matches:
c = item.get("cat") or {}
candidates.append({
"name": c.get("name"),
"breed": c.get("breed"),
"age": c.get("age"),
"sex_status": c.get("gender"),
"health": c.get("health"),
"vaccinations": c.get("vaccinations"),
"score": item.get("score"),
})
return f"""
# Role:
Professional AI Feline Health & Breed Consultant.
# Context:
You are an AI consultant for cat breeding & care.
# User's Cat:
{json.dumps(my_summary, ensure_ascii=False, indent=2)}
# Potential Match Cats:
{json.dumps(candidates, ensure_ascii=False, indent=2)}
# Output Format (JSON ONLY, no markdown block, no extra text):
# Write summary_md in English only.
{{
"summary_md": "≤100 English words, markdown allowed. Brief compatibility summary in English.",
"image_prompt": "≤100 English words, for image generation"
}}
""".strip()
# ----------------------------
# API: GET /api/match-result
# ----------------------------
@bp.get("/match-result")
def match_result():
user = require_jwt()
user_id = int(user["user_id"])
email = user["email"]
store = Storage(current_app.config["SQLITE_PATH"])
my_cat = store.get_cat_by_user_id(user_id)
if not my_cat:
return jsonify({
"status": "error",
"message": "No cat profile"
}), 404
# -------- 配對計分 --------
others = store.list_other_cats(user_id=user_id, limit=200)
# Only consider cats with a different gender than the user's feline
others = [c for c in others if _different_gender(my_cat.get("gender"), c.get("gender"))]
scored = sorted(
[_score(my_cat, c) for c in others],
key=lambda x: x["score"],
reverse=True
)
top_matches = _select_diverse_matches(scored, my_cat.get("breed"), limit=5)
# -------- Poe 文字分析 --------
review_md = "AI analysis is temporarily unavailable"
image_prompt = None
try:
logger.info("Building prompt for Poe analysis...")
prompt = _build_prompt(my_cat, top_matches)
logger.info(f"Prompt built successfully, length: {len(prompt)}")
logger.info("Calling analyze_with_poe...")
ai_result = analyze_with_poe(prompt)
logger.info(f"Poe analyze result: {ai_result}")
if isinstance(ai_result, dict):
review_md = ai_result.get("summary_md", review_md)
image_prompt = ai_result.get("image_prompt")
logger.info(f"Successfully extracted review_md and image_prompt")
else:
logger.warning(f"AI result is not a dict, got: {type(ai_result)}")
except Exception as e:
logger.exception(f"Poe analyze failed: {str(e)}")
current_app.logger.exception("Poe analyze failed")
# -------- OpenRouter 產圖 --------
kitten_image_url = None
generation_id = None
image_result = None
if image_prompt:
image_prompt = image_prompt.strip()
# 強制 ≤100 字
if len(image_prompt) > 100:
image_prompt = image_prompt[:100]
try:
logger.info(f"Generating image with prompt: {image_prompt[:50]}...")
image_result = generate_kitten_image(
image_prompt,
logger=current_app.logger
)
logger.info(f"Image generation result: {image_result}")
except Exception as e:
logger.exception(f"Image generation crashed: {str(e)}")
current_app.logger.exception("Image generation crashed")
else:
logger.info("No image prompt provided, skipping image generation")
if image_result and image_result.get("success"):
s3_uri = image_result.get("s3_uri")
if s3_uri:
store.update_cat_ai_match_photo(user_id, s3_uri)
kitten_image_url = (
resolve_stored_media_url(current_app.config, s3_uri)
if s3_uri
else image_result.get("image_url")
)
generation_id = image_result.get("generation_id")
else:
if image_result:
current_app.logger.warning(
f"Image generation failed: {image_result.get('error')}"
)
# -------- Response --------
return jsonify({
"status": "ok",
"user": {
"email": email,
"user_id": user_id
},
"your_feline": _cat_for_api(my_cat),
"match_review_md": review_md,
"kitten_image_url": kitten_image_url,
"potential_matches": [_match_item_for_api(x) for x in top_matches],
}), 200
# ----------------------------
# Prompt builder for offspring generation
# ----------------------------
def _build_offspring_prompt(my_cat: dict, match_cat: dict) -> str:
"""Build a prompt for generating offspring review and image for a specific cat pair."""
my_profile = my_cat.get("profile") or {}
match_profile = match_cat.get("profile") or {}
my_summary = {
"name": my_cat.get("name"),
"breed": my_cat.get("breed"),
"age": my_cat.get("age"),
"sex_status": my_cat.get("gender"),
"health": my_cat.get("health"),
"vaccinations": my_cat.get("vaccinations"),
}
match_summary = {
"name": match_cat.get("name"),
"breed": match_cat.get("breed"),
"age": match_cat.get("age"),
"sex_status": match_cat.get("gender"),
"health": match_cat.get("health"),
"vaccinations": match_cat.get("vaccinations"),
}
return f"""
# Role:
Professional AI Feline Genetics & Breeding Consultant.
# Context:
You are analyzing the potential offspring of two specific cats. Provide insights about what their kitten might look like, inherit traits, and health considerations.
# Parent Cat 1 (User's Cat):
{json.dumps(my_summary, ensure_ascii=False, indent=2)}
# Parent Cat 2 (Selected Match):
{json.dumps(match_summary, ensure_ascii=False, indent=2)}
# Task:
Analyze the potential offspring of these two cats. Consider:
1. Likely breed characteristics and appearance
2. Potential health traits inherited from parents
3. Expected temperament based on parent breeds
# Output Format (JSON ONLY, no markdown block, no extra text):
# Write summary_md in English only.
{{
"summary_md": "≤200 English words, markdown allowed. Analysis of the potential offspring in English: appearance, health predictions, and care recommendations.",
"image_prompt": "≤200 English words describing a realistic cute kitten that would result from breeding a {my_cat.get('breed', 'cat')} with a {match_cat.get('breed', 'cat')}, include specific appearance details like fur color, pattern, eye color, and body type"
}}
""".strip()
# ----------------------------
# API: POST /api/generate-offspring
# ----------------------------
@bp.post("/generate-offspring")
def generate_offspring():
"""Generate AI review and kitten image for a specific cat pair."""
user = require_jwt()
user_id = int(user["user_id"])
data = request.get_json(silent=True) or {}
match_cat_id = data.get("match_cat_id")
if not match_cat_id:
return jsonify({
"status": "error",
"message": "match_cat_id is required"
}), 400
store = Storage(current_app.config["SQLITE_PATH"])
# Get user's cat
my_cat = store.get_cat_by_user_id(user_id)
if not my_cat:
return jsonify({
"status": "error",
"message": "No cat profile found"
}), 404
# Get the matched cat by ID
match_cat = None
others = store.list_other_cats(user_id=user_id, limit=200)
for cat in others:
if cat.get("id") == match_cat_id:
match_cat = cat
break
if not match_cat:
return jsonify({
"status": "error",
"message": "Match cat not found"
}), 404
if not _different_gender(my_cat.get("gender"), match_cat.get("gender")):
return jsonify({
"status": "error",
"message": "Your Feline and Potential Match must have different genders"
}), 400
# -------- Poe AI Analysis for this pair --------
review_md = "AI analysis is temporarily unavailable"
image_prompt = None
try:
logger.info(f"Building offspring prompt for cats: {my_cat.get('name')} + {match_cat.get('name')}")
prompt = _build_offspring_prompt(my_cat, match_cat)
logger.info(f"Offspring prompt built, length: {len(prompt)}")
logger.info("Calling analyze_with_poe for offspring...")
ai_result = analyze_with_poe(prompt)
logger.info(f"Offspring Poe result: {ai_result}")
if isinstance(ai_result, dict):
review_md = ai_result.get("summary_md", review_md)
image_prompt = ai_result.get("image_prompt")
logger.info(f"Extracted offspring review and image_prompt")
else:
logger.warning(f"AI result is not a dict, got: {type(ai_result)}")
except Exception as e:
logger.exception(f"Offspring Poe analyze failed: {str(e)}")
current_app.logger.exception("Offspring Poe analyze failed")
# -------- OpenRouter Image Generation --------
kitten_image_url = None
generation_id = None
image_result = None
if image_prompt:
image_prompt = image_prompt.strip()
# Limit to 100 words
words = image_prompt.split()
if len(words) > 100:
image_prompt = " ".join(words[:100])
try:
logger.info(f"Generating offspring image with prompt: {image_prompt[:80]}...")
image_result = generate_kitten_image(
image_prompt,
logger=current_app.logger
)
logger.info(f"Offspring image generation result: {image_result}")
except Exception as e:
logger.exception(f"Offspring image generation crashed: {str(e)}")
current_app.logger.exception("Offspring image generation crashed")
else:
logger.info("No image prompt for offspring, skipping image generation")
if image_result and image_result.get("success"):
s3_uri = image_result.get("s3_uri")
if s3_uri:
store.upsert_offspring_ai_photo(user_id, int(match_cat_id), s3_uri)
kitten_image_url = (
resolve_stored_media_url(current_app.config, s3_uri)
if s3_uri
else image_result.get("image_url")
)
generation_id = image_result.get("generation_id")
else:
if image_result:
current_app.logger.warning(
f"Offspring image generation failed: {image_result.get('error')}"
)
# -------- Response --------
return jsonify({
"status": "ok",
"match_cat_id": match_cat_id,
"match_cat_name": match_cat.get("name"),
"review_md": review_md,
"kitten_image_url": kitten_image_url,
"generation_id": generation_id,
}), 200
Artículos relacionados
__init__.py
__init__.py — python source code from the Ricky learning materials (Ricky/Cat Project Final/back_web-4/app/__init__.py).
Leer artículo →auth.py
auth.py — python source code from the Ricky learning materials (Ricky/Cat Project Final/back_web-4/app/admin/auth.py).
Leer artículo →routes.py
routes.py — python source code from the Ricky learning materials (Ricky/Cat Project Final/back_web-4/app/admin/routes.py).
Leer artículo →config.py
config.py — python source code from the Ricky learning materials (Ricky/Cat Project Final/back_web-4/app/config.py).
Leer artículo →admin_ingest.py
admin_ingest.py — python source code from the Ricky learning materials (Ricky/Cat Project Final/back_web-4/app/routes/admin_ingest.py).
Leer artículo →admin_stats.py
admin_stats.py — python source code from the Ricky learning materials (Ricky/Cat Project Final/back_web-4/app/routes/admin_stats.py).
Leer artículo →