| """ |
| 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": "<one of madhubani|warli|pattachitra|mughal|tanjore>", |
| "reason": "<one-sentence cultural rationale>", |
| "confidence": <float 0..1>}""" |
|
|
|
|
| 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]) |
|
|
| |
| return self._heuristic_recommend(user_prompt) |
|
|
| def _safe_parse(self, content: str) -> Dict | None: |
| |
| try: |
| return json.loads(content) |
| except Exception: |
| pass |
| |
| 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 |
|
|
| |
| best = max(scores, key=lambda k: (scores[k], -list(scores).index(k))) |
| if scores[best] == 0: |
| best = "madhubani" |
|
|
| 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", |
| } |
|
|