""" StyleAdvisor — recommends a heritage style based on user prompt. Uses the free AMD Qwen/DeepSeek API. Falls back to a keyword heuristic when the API is unavailable (deterministic, same prompt → same recommendation). """ from __future__ import annotations import json import logging import re from typing import Dict from .base import AgentClient, AgentResponse from config.styles import HERITAGE_STYLES, StyleSpec, get_style, list_styles log = logging.getLogger(__name__) SYSTEM_PROMPT = """You are a cultural art advisor specializing in Indian heritage painting traditions. You recommend ONE of these five styles based on the user's text description: - madhubani: Bihar folk art, geometric patterns, nature and mythology motifs. - warli: Maharashtra tribal art, white-on-ochre, stick figures, dance and hunting scenes. - pattachitra: Odisha scroll painting, mythological narratives (Jagannath, Krishna). - mughal: Mughal court miniature, fine detail, gold leaf, elevated viewpoint. - tanjore: Tamil Nadu devotional icon, gold leaf, frontal symmetry, deity portrait. Respond with STRICT JSON only, no markdown: {"style": "", "reason": "", "confidence": }""" class StyleAdvisor: def __init__(self, client: AgentClient | None = None) -> None: self.client = client or AgentClient(temperature=0.3, max_tokens=400) def recommend(self, user_prompt: str) -> Dict: """Return {style, reason, confidence, source}.""" if self.client.enabled: resp: AgentResponse = self.client.chat( system_prompt=SYSTEM_PROMPT, user_prompt=f"User prompt: {user_prompt!r}\n\nRecommend a style as JSON.", ) if resp.ok: parsed = self._safe_parse(resp.content) if parsed and parsed.get("style") in HERITAGE_STYLES: parsed["source"] = "amd_agent" return parsed log.warning("Agent JSON parse failed: %s", resp.content[:200]) # Fallback heuristic return self._heuristic_recommend(user_prompt) def _safe_parse(self, content: str) -> Dict | None: # Try strict JSON first try: return json.loads(content) except Exception: pass # Extract first {...} block m = re.search(r"\{[^{}]*\}", content, re.DOTALL) if m: try: return json.loads(m.group(0)) except Exception: pass return None @staticmethod def _heuristic_recommend(user_prompt: str) -> Dict: """Deterministic keyword-based fallback.""" text = user_prompt.lower() scores = {sid: 0 for sid in HERITAGE_STYLES} keyword_map = { "madhubani": ["nature", "tree", "fish", "peacock", "sun", "moon", "banyan", "krishna", "woman", "folk", "village"], "warli": ["dance", "tribal", "hunter", "village", "rural", "stick", "community", "wedding", "tarpa", "celebration"], "pattachitra": ["jagannath", "krishna", "mythology", "story", "scroll", "odisha", "temple", "narrative", "rama", "vishnu"], "mughal": ["court", "king", "emperor", "palace", "battle", "garden", "prince", "princess", "hunt", "persian", "mughal"], "tanjore": ["deity", "god", "goddess", "temple", "devotion", "krishna", "shiva", "vishnu", "laxmi", "saraswati", "icon", "prayer"], } for sid, kws in keyword_map.items(): for kw in kws: if kw in text: scores[sid] += 1 # Pick top; tie-break by canonical order (madhubani first) best = max(scores, key=lambda k: (scores[k], -list(scores).index(k))) if scores[best] == 0: best = "madhubani" # safe default style: StyleSpec = get_style(best) return { "style": best, "reason": f"Heuristic match on cultural keywords ({style.display_name}).", "confidence": min(0.5 + scores[best] * 0.1, 0.9), "source": "heuristic_fallback", }