indic-heritage-studio / agents /style_advisor.py
Dev2506's picture
Add files using upload-large-folder tool
15d68eb verified
Raw
History Blame Contribute Delete
4.3 kB
"""
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])
# 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",
}