Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -537,9 +537,9 @@ def _try_gemini_ground(q: str):
|
|
| 537 |
f"Input: {q!r}"
|
| 538 |
)
|
| 539 |
resp = GENAI_CLIENT.models.generate_content(
|
| 540 |
-
model="gemini-
|
| 541 |
contents=prompt,
|
| 542 |
-
|
| 543 |
)
|
| 544 |
txt = (resp.text or "").strip()
|
| 545 |
if txt.startswith("```"):
|
|
@@ -570,8 +570,8 @@ def _try_gemini_ground(q: str):
|
|
| 570 |
if GENAI_LEGACY:
|
| 571 |
try:
|
| 572 |
model = GENAI_LEGACY.GenerativeModel(
|
| 573 |
-
"gemini-
|
| 574 |
-
tools=
|
| 575 |
)
|
| 576 |
prompt = (
|
| 577 |
"Resolve a Clash Royale card string (typos possible). "
|
|
@@ -1483,7 +1483,7 @@ def _gemini_generate_text(prompt: str) -> str | None:
|
|
| 1483 |
if GENAI_CLIENT:
|
| 1484 |
try:
|
| 1485 |
resp = GENAI_CLIENT.models.generate_content(
|
| 1486 |
-
model="gemini-
|
| 1487 |
contents=prompt
|
| 1488 |
)
|
| 1489 |
return (resp.text or "").strip()
|
|
@@ -1492,46 +1492,12 @@ def _gemini_generate_text(prompt: str) -> str | None:
|
|
| 1492 |
# legacy
|
| 1493 |
if GENAI_LEGACY:
|
| 1494 |
try:
|
| 1495 |
-
out = GENAI_LEGACY.GenerativeModel("gemini-
|
| 1496 |
return (out.text or "").strip()
|
| 1497 |
except Exception as e:
|
| 1498 |
_set_last_error(f"Gemini error (legacy): {e}")
|
| 1499 |
return None
|
| 1500 |
|
| 1501 |
-
# =========== NEW: Gemini helper for chat with Google Search enabled ===========
|
| 1502 |
-
def _gemini_generate_with_search(prompt: str) -> str | None:
|
| 1503 |
-
"""Helper that enables Google Search tool for grounded, factual answers."""
|
| 1504 |
-
# new SDK
|
| 1505 |
-
if GENAI_CLIENT and GENAI_TYPES:
|
| 1506 |
-
try:
|
| 1507 |
-
Tool = GENAI_TYPES.Tool
|
| 1508 |
-
GoogleSearch = GENAI_TYPES.GoogleSearch
|
| 1509 |
-
generation_config = GENAI_TYPES.GenerationConfig(tools=[Tool(google_search=GoogleSearch())])
|
| 1510 |
-
|
| 1511 |
-
resp = GENAI_CLIENT.models.generate_content(
|
| 1512 |
-
model="gemini-1.5-flash",
|
| 1513 |
-
contents=prompt,
|
| 1514 |
-
generation_config=generation_config,
|
| 1515 |
-
)
|
| 1516 |
-
return (resp.text or "").strip()
|
| 1517 |
-
except Exception as e:
|
| 1518 |
-
_set_last_error(f"Gemini error (new SDK with search): {e}")
|
| 1519 |
-
return "Sorry, I encountered an error while searching for an answer."
|
| 1520 |
-
# legacy SDK
|
| 1521 |
-
if GENAI_LEGACY:
|
| 1522 |
-
try:
|
| 1523 |
-
model = GENAI_LEGACY.GenerativeModel(
|
| 1524 |
-
"gemini-1.5-flash",
|
| 1525 |
-
tools=[GENAI_LEGACY.Tool.from_google_search_retrieval(GENAI_LEGACY.GoogleSearch())]
|
| 1526 |
-
)
|
| 1527 |
-
out = model.generate_content(prompt)
|
| 1528 |
-
return (out.text or "").strip()
|
| 1529 |
-
except Exception as e:
|
| 1530 |
-
_set_last_error(f"Gemini error (legacy with search): {e}")
|
| 1531 |
-
return "Sorry, I encountered an error while searching for an answer."
|
| 1532 |
-
return "Gemini with search is not configured."
|
| 1533 |
-
|
| 1534 |
-
|
| 1535 |
def gem_coach(pack: dict, history=None, question=None):
|
| 1536 |
"""
|
| 1537 |
Coach-style, trophy-range–aware analysis. ONLY uses data in pack.
|
|
@@ -1569,8 +1535,10 @@ def gem_coach(pack: dict, history=None, question=None):
|
|
| 1569 |
"CRITICAL: Use ONLY the cards/decks present in the JSON. Do NOT invent cards or stats. "
|
| 1570 |
"Respect owned_only/min_level constraints and keep suggested average elixir reasonable.\n\n"
|
| 1571 |
|
|
|
|
| 1572 |
"**IMPORTANT: Use the `deck_card_details` section to understand the function of each card in the user's deck. This is your primary source of truth for what each card does.**\n\n"
|
| 1573 |
-
|
|
|
|
| 1574 |
"Context you MUST use:\n"
|
| 1575 |
"- player.trophies and bracket.\n"
|
| 1576 |
"- trophy_range_meta.* are what the user is actually facing now.\n"
|
|
@@ -1604,31 +1572,21 @@ def gem_coach(pack: dict, history=None, question=None):
|
|
| 1604 |
_set_last_error(f"Gemini parse error: {e}")
|
| 1605 |
return None
|
| 1606 |
else: # Follow-up question (compose a single prompt with context + short history)
|
| 1607 |
-
|
| 1608 |
-
|
| 1609 |
-
"You are a helpful Clash Royale coach. Concisely answer the user's follow-up question.",
|
| 1610 |
-
"Use the provided JSON data for context about the user's deck, performance, and the initial meta analysis.",
|
| 1611 |
-
"If the user asks a factual question about the game (e.g., card stats, damage, interactions) that is not in the provided JSON, you MUST use the search tool to find the most accurate and up-to-date information.",
|
| 1612 |
-
"Always prioritize factual accuracy."
|
| 1613 |
-
]
|
| 1614 |
-
# Seed with initial analysis if available
|
| 1615 |
if history and history[0] and history[0][1]:
|
| 1616 |
-
|
| 1617 |
-
|
| 1618 |
-
# Add last 4 user/model turns for context
|
| 1619 |
for user_msg, model_msg in history[-4:]:
|
| 1620 |
if user_msg:
|
| 1621 |
convo_lines.append("User: " + user_msg)
|
| 1622 |
-
if model_msg
|
| 1623 |
convo_lines.append("Coach: " + model_msg)
|
|
|
|
|
|
|
| 1624 |
|
| 1625 |
-
|
| 1626 |
-
convo_lines.append("\nUser's new question: " + (question or ""))
|
| 1627 |
-
|
| 1628 |
-
txt = _gemini_generate_with_search("\n\n".join(convo_lines))
|
| 1629 |
return txt or "Sorry, I couldn't generate a response."
|
| 1630 |
-
# ========= END: MODIFIED FOLLOW-UP LOGIC ===========
|
| 1631 |
-
|
| 1632 |
|
| 1633 |
# =========================
|
| 1634 |
# Smart Builder (numeric core; semantics via Gemini)
|
|
@@ -1647,15 +1605,14 @@ def smart_builder(
|
|
| 1647 |
player_info_for_pack=None,
|
| 1648 |
micro_weight=0.7,
|
| 1649 |
include_top=True,
|
| 1650 |
-
recent_matches_for_pack=None
|
| 1651 |
-
inv=None # Accept inventory object
|
| 1652 |
):
|
| 1653 |
deck = canonicalize_card_list(list(deck_cards or []))
|
| 1654 |
if not deck:
|
| 1655 |
return "Pick at least one card first.", None
|
| 1656 |
|
| 1657 |
# ================================================================= #
|
| 1658 |
-
# ========= START: NEW CODE TO FETCH AND INJECT DETAILS ========= #
|
| 1659 |
# ================================================================= #
|
| 1660 |
|
| 1661 |
deck_card_details = []
|
|
@@ -1671,7 +1628,7 @@ def smart_builder(
|
|
| 1671 |
})
|
| 1672 |
|
| 1673 |
# =============================================================== #
|
| 1674 |
-
# ========= END: NEW CODE TO FETCH AND INJECT DETAILS =========
|
| 1675 |
# =============================================================== #
|
| 1676 |
|
| 1677 |
|
|
@@ -1739,8 +1696,7 @@ def smart_builder(
|
|
| 1739 |
existing = set(deck)
|
| 1740 |
base_avg = avg_elixir(deck)
|
| 1741 |
elixir_low, elixir_high = 3.0, 4.5
|
| 1742 |
-
|
| 1743 |
-
pool = candidate_cards(inv or STATE.inv, min_level=min_level, owned_only=owned_only)
|
| 1744 |
suggestions_seed = []
|
| 1745 |
|
| 1746 |
for c in pool:
|
|
@@ -1770,9 +1726,9 @@ def smart_builder(
|
|
| 1770 |
|
| 1771 |
# Build LLM pack (facts only) with priority policy + local meta + hard counters
|
| 1772 |
priority_policy = {"micro_weight": float(micro_weight), "include_top": bool(include_top)}
|
| 1773 |
-
pack = build_llm_pack(deck, df, ctx_notes, top_seed,
|
| 1774 |
|
| 1775 |
-
# Inject the details into the final pack object
|
| 1776 |
pack["deck_card_details"] = deck_card_details
|
| 1777 |
|
| 1778 |
lines = []
|
|
@@ -1853,7 +1809,7 @@ def smart_builder(
|
|
| 1853 |
for r in ranked[:10]:
|
| 1854 |
badges = " ".join(f"`{b}`" for b in (r.get("badges") or []))
|
| 1855 |
reason = r.get('reason') or ""
|
| 1856 |
-
lines.append(f"-
|
| 1857 |
edits = result.get("edits_for_meta") or []
|
| 1858 |
if edits:
|
| 1859 |
lines.append("\n**Edits to beat your local meta**")
|
|
@@ -1864,12 +1820,12 @@ def smart_builder(
|
|
| 1864 |
if tech:
|
| 1865 |
lines.append("\n**Tech choices**")
|
| 1866 |
for r in tech[:6]:
|
| 1867 |
-
lines.append(f"-
|
| 1868 |
qf = result.get("quick_fixes") or []
|
| 1869 |
if qf:
|
| 1870 |
lines.append("\n**Quick +1 fixes**")
|
| 1871 |
for r in qf[:6]:
|
| 1872 |
-
lines.append(f"-
|
| 1873 |
if result.get("warnings"):
|
| 1874 |
for w in result["warnings"]:
|
| 1875 |
lines.append("⚠️ " + w)
|
|
@@ -2029,7 +1985,7 @@ def player_snapshot_lines(p, tag):
|
|
| 2029 |
for k in ("targetType","hitSpeed","dmgType"):
|
| 2030 |
if k in ext: ext_bits.append(f"{k}={ext[k]}")
|
| 2031 |
extra = (" | " + ", ".join(ext_bits)) if ext_bits else ""
|
| 2032 |
-
lines.append(f"-
|
| 2033 |
if champs>1: lines.append(f"⚠️ Champions in deck: {champs} (most formats allow only 1)")
|
| 2034 |
if evos>0: lines.append(f"Evolution-ready in deck: {evos}")
|
| 2035 |
else:
|
|
@@ -2446,8 +2402,6 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
|
|
| 2446 |
"Tip: run **Fetch & Analyze** first to seed meta/co-occurrence for stronger suggestions."
|
| 2447 |
)
|
| 2448 |
use_gemini = gr.Checkbox(label="Explain & rank with Gemini", value=True)
|
| 2449 |
-
# =========== NEW: Unlocked cards checkbox ===========
|
| 2450 |
-
recommend_owned_only_chk = gr.Checkbox(label="Only recommend cards I have unlocked", value=False)
|
| 2451 |
with gr.Row():
|
| 2452 |
micro_w_slider = gr.Slider(0.0, 1.0, value=0.7, step=0.05, label="Weight on trophy-range micro-meta")
|
| 2453 |
include_top_chk = gr.Checkbox(label="Blend in top-players meta", value=True)
|
|
@@ -2455,9 +2409,8 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
|
|
| 2455 |
builder_md = gr.Markdown()
|
| 2456 |
|
| 2457 |
with gr.Column(visible=False) as chat_interface:
|
| 2458 |
-
chatbot = gr.Chatbot(label="Follow-up Coach Chat
|
| 2459 |
-
chat_input = gr.Textbox(label="Ask a follow-up question", placeholder="e.g.,
|
| 2460 |
-
|
| 2461 |
|
| 2462 |
# ================= Rankings =================
|
| 2463 |
with gr.Tab("Rankings & Top Decks", id=1):
|
|
@@ -2511,11 +2464,11 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
|
|
| 2511 |
wr_plot2 = gr.Plot()
|
| 2512 |
|
| 2513 |
gr.Markdown("---")
|
| 2514 |
-
gr.Markdown("### Card Lookup (handles typos + learns unknown cards)")
|
| 2515 |
with gr.Row():
|
| 2516 |
-
lookup_tb = gr.Textbox(label="Card lookup", placeholder="e.g., suspious bush")
|
| 2517 |
-
lookup_btn = gr.Button("Lookup Card Info")
|
| 2518 |
-
lookup_md = gr.Markdown()
|
| 2519 |
|
| 2520 |
# ================= Player & Forecast =================
|
| 2521 |
with gr.Tab("Player & Forecast", id=4):
|
|
@@ -2598,8 +2551,7 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
|
|
| 2598 |
gem_tb.change(_set_gem, [gem_tb], [gem_info])
|
| 2599 |
|
| 2600 |
# --- DECK BUILDER TAB ---
|
| 2601 |
-
|
| 2602 |
-
def _from_tag_rich(tag, use_gem, micro_w, include_top, owned_only):
|
| 2603 |
if not tag:
|
| 2604 |
return "Enter a player tag in the sidebar.", None, None, gr.update(visible=False), None
|
| 2605 |
if not STATE.api_token:
|
|
@@ -2611,23 +2563,8 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
|
|
| 2611 |
p = api.player(t)
|
| 2612 |
except Exception as e:
|
| 2613 |
return f"Error: {e}", None, None, gr.update(visible=False), None
|
| 2614 |
-
|
| 2615 |
-
# =========== NEW: Fetch and store player's card inventory ===========
|
| 2616 |
-
try:
|
| 2617 |
-
STATE.inv = Inventory()
|
| 2618 |
-
for card_data in p.get("cards", []):
|
| 2619 |
-
cname = jname(card_data.get("name"))
|
| 2620 |
-
if cname:
|
| 2621 |
-
STATE.inv.card[cname] = {
|
| 2622 |
-
"owned": True,
|
| 2623 |
-
"level": card_data.get("level", 0)
|
| 2624 |
-
}
|
| 2625 |
-
except Exception as e:
|
| 2626 |
-
# Non-critical error, can proceed without inventory
|
| 2627 |
-
_set_last_error(f"Could not parse player inventory: {e}")
|
| 2628 |
|
| 2629 |
-
|
| 2630 |
-
# Collect recent matches for LLM analysis
|
| 2631 |
recent_matches = collect_recent_matches_for_llm(api, t)
|
| 2632 |
|
| 2633 |
header_lines = player_snapshot_lines(p, t)
|
|
@@ -2666,35 +2603,39 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
|
|
| 2666 |
use_bracket=True, bracket_center=p.get("trophies"),
|
| 2667 |
player_info_for_pack=player_info_for_pack,
|
| 2668 |
micro_weight=float(micro_w), include_top=bool(include_top),
|
| 2669 |
-
recent_matches_for_pack=recent_matches
|
| 2670 |
-
owned_only=bool(owned_only), # Pass the new parameter
|
| 2671 |
-
inv=STATE.inv # Pass the fetched inventory
|
| 2672 |
)
|
| 2673 |
final_md = "\n".join(header_lines + ["", *perf_lines, "", coach_block])
|
| 2674 |
|
| 2675 |
-
|
|
|
|
|
|
|
|
|
|
| 2676 |
|
| 2677 |
-
return final_md, pack,
|
| 2678 |
|
| 2679 |
-
def chat_response(message,
|
|
|
|
| 2680 |
if not pack_state:
|
| 2681 |
-
|
| 2682 |
-
return history, coach_hist_state
|
| 2683 |
|
| 2684 |
-
# Call coach with the pair-style history
|
| 2685 |
-
reply = gem_coach(pack_state, history=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2686 |
|
| 2687 |
-
# Update
|
| 2688 |
-
|
| 2689 |
-
# Update internal state history
|
| 2690 |
-
new_coach_hist = coach_hist_state + [(message, reply)]
|
| 2691 |
|
| 2692 |
-
return
|
| 2693 |
|
| 2694 |
-
# =========== MODIFIED: Pass new checkbox state to handler ===========
|
| 2695 |
build_from_tag_btn.click(
|
| 2696 |
_from_tag_rich,
|
| 2697 |
-
[player_tag_state, use_gemini, micro_w_slider, include_top_chk
|
| 2698 |
[builder_md, builder_pack_state, chatbot, chat_interface, coach_history_state]
|
| 2699 |
)
|
| 2700 |
|
|
@@ -2736,6 +2677,7 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
|
|
| 2736 |
# --- CARD STATS TAB ---
|
| 2737 |
meta_btn.click(recompute_meta, [], [meta_df, meta_plot, wr_plot2])
|
| 2738 |
|
|
|
|
| 2739 |
def _lookup_card_handler(q):
|
| 2740 |
q = (q or "").strip()
|
| 2741 |
if not q:
|
|
@@ -2745,7 +2687,7 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
|
|
| 2745 |
info = lookup_card_details(q)
|
| 2746 |
if not info:
|
| 2747 |
return f"Sorry, I couldn't find info for **{q}**."
|
| 2748 |
-
lines = [f"
|
| 2749 |
if info.get("summary"):
|
| 2750 |
lines.append(info["summary"])
|
| 2751 |
# show basics if we have them
|
|
@@ -2756,7 +2698,7 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
|
|
| 2756 |
mel = info.get("maxEvolutionLevel", 0)
|
| 2757 |
if mel: bits.append(f"Max Evolution Level: {mel}")
|
| 2758 |
if bits:
|
| 2759 |
-
lines.append("
|
| 2760 |
# citations
|
| 2761 |
srcs = info.get("sources") or []
|
| 2762 |
if srcs:
|
|
@@ -2813,4 +2755,4 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
|
|
| 2813 |
|
| 2814 |
|
| 2815 |
if __name__ == "__main__":
|
| 2816 |
-
demo.launch()
|
|
|
|
| 537 |
f"Input: {q!r}"
|
| 538 |
)
|
| 539 |
resp = GENAI_CLIENT.models.generate_content(
|
| 540 |
+
model="gemini-2.5-flash",
|
| 541 |
contents=prompt,
|
| 542 |
+
config=cfg,
|
| 543 |
)
|
| 544 |
txt = (resp.text or "").strip()
|
| 545 |
if txt.startswith("```"):
|
|
|
|
| 570 |
if GENAI_LEGACY:
|
| 571 |
try:
|
| 572 |
model = GENAI_LEGACY.GenerativeModel(
|
| 573 |
+
"gemini-2.5-flash",
|
| 574 |
+
tools={"google_search": {}}
|
| 575 |
)
|
| 576 |
prompt = (
|
| 577 |
"Resolve a Clash Royale card string (typos possible). "
|
|
|
|
| 1483 |
if GENAI_CLIENT:
|
| 1484 |
try:
|
| 1485 |
resp = GENAI_CLIENT.models.generate_content(
|
| 1486 |
+
model="gemini-2.5-flash",
|
| 1487 |
contents=prompt
|
| 1488 |
)
|
| 1489 |
return (resp.text or "").strip()
|
|
|
|
| 1492 |
# legacy
|
| 1493 |
if GENAI_LEGACY:
|
| 1494 |
try:
|
| 1495 |
+
out = GENAI_LEGACY.GenerativeModel("gemini-2.5-flash").generate_content(prompt)
|
| 1496 |
return (out.text or "").strip()
|
| 1497 |
except Exception as e:
|
| 1498 |
_set_last_error(f"Gemini error (legacy): {e}")
|
| 1499 |
return None
|
| 1500 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1501 |
def gem_coach(pack: dict, history=None, question=None):
|
| 1502 |
"""
|
| 1503 |
Coach-style, trophy-range–aware analysis. ONLY uses data in pack.
|
|
|
|
| 1535 |
"CRITICAL: Use ONLY the cards/decks present in the JSON. Do NOT invent cards or stats. "
|
| 1536 |
"Respect owned_only/min_level constraints and keep suggested average elixir reasonable.\n\n"
|
| 1537 |
|
| 1538 |
+
# ========= START: PROMPT MODIFICATION (UPDATED CODE) ========= #
|
| 1539 |
"**IMPORTANT: Use the `deck_card_details` section to understand the function of each card in the user's deck. This is your primary source of truth for what each card does.**\n\n"
|
| 1540 |
+
# ========= END: PROMPT MODIFICATION (UPDATED CODE) ========= #
|
| 1541 |
+
|
| 1542 |
"Context you MUST use:\n"
|
| 1543 |
"- player.trophies and bracket.\n"
|
| 1544 |
"- trophy_range_meta.* are what the user is actually facing now.\n"
|
|
|
|
| 1572 |
_set_last_error(f"Gemini parse error: {e}")
|
| 1573 |
return None
|
| 1574 |
else: # Follow-up question (compose a single prompt with context + short history)
|
| 1575 |
+
convo_lines = []
|
| 1576 |
+
# seed with initial analysis if available
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1577 |
if history and history[0] and history[0][1]:
|
| 1578 |
+
convo_lines.append("Initial analysis from coach:\n" + history[0][1])
|
| 1579 |
+
# add last 4 user/model turns for context
|
|
|
|
| 1580 |
for user_msg, model_msg in history[-4:]:
|
| 1581 |
if user_msg:
|
| 1582 |
convo_lines.append("User: " + user_msg)
|
| 1583 |
+
if model_msg:
|
| 1584 |
convo_lines.append("Coach: " + model_msg)
|
| 1585 |
+
convo_lines.append("\nUse ONLY data in this JSON pack for facts:\n" + json.dumps(pack, separators=(',',':')))
|
| 1586 |
+
convo_lines.append("\nUser question: " + (question or ""))
|
| 1587 |
|
| 1588 |
+
txt = _gemini_generate_text("\n\n".join(convo_lines))
|
|
|
|
|
|
|
|
|
|
| 1589 |
return txt or "Sorry, I couldn't generate a response."
|
|
|
|
|
|
|
| 1590 |
|
| 1591 |
# =========================
|
| 1592 |
# Smart Builder (numeric core; semantics via Gemini)
|
|
|
|
| 1605 |
player_info_for_pack=None,
|
| 1606 |
micro_weight=0.7,
|
| 1607 |
include_top=True,
|
| 1608 |
+
recent_matches_for_pack=None # New parameter
|
|
|
|
| 1609 |
):
|
| 1610 |
deck = canonicalize_card_list(list(deck_cards or []))
|
| 1611 |
if not deck:
|
| 1612 |
return "Pick at least one card first.", None
|
| 1613 |
|
| 1614 |
# ================================================================= #
|
| 1615 |
+
# ========= START: NEW CODE TO FETCH AND INJECT DETAILS (UPDATED CODE) ========= #
|
| 1616 |
# ================================================================= #
|
| 1617 |
|
| 1618 |
deck_card_details = []
|
|
|
|
| 1628 |
})
|
| 1629 |
|
| 1630 |
# =============================================================== #
|
| 1631 |
+
# ========= END: NEW CODE TO FETCH AND INJECT DETAILS (UPDATED CODE) ========= #
|
| 1632 |
# =============================================================== #
|
| 1633 |
|
| 1634 |
|
|
|
|
| 1696 |
existing = set(deck)
|
| 1697 |
base_avg = avg_elixir(deck)
|
| 1698 |
elixir_low, elixir_high = 3.0, 4.5
|
| 1699 |
+
pool = candidate_cards(STATE.inv, min_level=min_level, owned_only=owned_only)
|
|
|
|
| 1700 |
suggestions_seed = []
|
| 1701 |
|
| 1702 |
for c in pool:
|
|
|
|
| 1726 |
|
| 1727 |
# Build LLM pack (facts only) with priority policy + local meta + hard counters
|
| 1728 |
priority_policy = {"micro_weight": float(micro_weight), "include_top": bool(include_top)}
|
| 1729 |
+
pack = build_llm_pack(deck, df, ctx_notes, top_seed, STATE.inv, min_level, owned_only, coo, player_info_for_pack or {}, priority_policy, recent_matches=recent_matches_for_pack)
|
| 1730 |
|
| 1731 |
+
# Inject the details into the final pack object (UPDATED CODE)
|
| 1732 |
pack["deck_card_details"] = deck_card_details
|
| 1733 |
|
| 1734 |
lines = []
|
|
|
|
| 1809 |
for r in ranked[:10]:
|
| 1810 |
badges = " ".join(f"`{b}`" for b in (r.get("badges") or []))
|
| 1811 |
reason = r.get('reason') or ""
|
| 1812 |
+
lines.append(f"- {r.get('card')}: {reason} {badges}")
|
| 1813 |
edits = result.get("edits_for_meta") or []
|
| 1814 |
if edits:
|
| 1815 |
lines.append("\n**Edits to beat your local meta**")
|
|
|
|
| 1820 |
if tech:
|
| 1821 |
lines.append("\n**Tech choices**")
|
| 1822 |
for r in tech[:6]:
|
| 1823 |
+
lines.append(f"- {r.get('card')}: {r.get('why')}")
|
| 1824 |
qf = result.get("quick_fixes") or []
|
| 1825 |
if qf:
|
| 1826 |
lines.append("\n**Quick +1 fixes**")
|
| 1827 |
for r in qf[:6]:
|
| 1828 |
+
lines.append(f"- {r.get('card')}: {r.get('reason')}")
|
| 1829 |
if result.get("warnings"):
|
| 1830 |
for w in result["warnings"]:
|
| 1831 |
lines.append("⚠️ " + w)
|
|
|
|
| 1985 |
for k in ("targetType","hitSpeed","dmgType"):
|
| 1986 |
if k in ext: ext_bits.append(f"{k}={ext[k]}")
|
| 1987 |
extra = (" | " + ", ".join(ext_bits)) if ext_bits else ""
|
| 1988 |
+
lines.append(f"- {nm}: roles={('/'.join(r) or '—')} | tags={('/'.join(sr) or '—')}{extra}")
|
| 1989 |
if champs>1: lines.append(f"⚠️ Champions in deck: {champs} (most formats allow only 1)")
|
| 1990 |
if evos>0: lines.append(f"Evolution-ready in deck: {evos}")
|
| 1991 |
else:
|
|
|
|
| 2402 |
"Tip: run **Fetch & Analyze** first to seed meta/co-occurrence for stronger suggestions."
|
| 2403 |
)
|
| 2404 |
use_gemini = gr.Checkbox(label="Explain & rank with Gemini", value=True)
|
|
|
|
|
|
|
| 2405 |
with gr.Row():
|
| 2406 |
micro_w_slider = gr.Slider(0.0, 1.0, value=0.7, step=0.05, label="Weight on trophy-range micro-meta")
|
| 2407 |
include_top_chk = gr.Checkbox(label="Blend in top-players meta", value=True)
|
|
|
|
| 2409 |
builder_md = gr.Markdown()
|
| 2410 |
|
| 2411 |
with gr.Column(visible=False) as chat_interface:
|
| 2412 |
+
chatbot = gr.Chatbot(label="Follow-up Coach Chat", height=400, type="messages")
|
| 2413 |
+
chat_input = gr.Textbox(label="Ask a follow-up question", placeholder="e.g., What's a good replacement for my Knight?")
|
|
|
|
| 2414 |
|
| 2415 |
# ================= Rankings =================
|
| 2416 |
with gr.Tab("Rankings & Top Decks", id=1):
|
|
|
|
| 2464 |
wr_plot2 = gr.Plot()
|
| 2465 |
|
| 2466 |
gr.Markdown("---")
|
| 2467 |
+
gr.Markdown("### Card Lookup (handles typos + learns unknown cards)") # NEW
|
| 2468 |
with gr.Row():
|
| 2469 |
+
lookup_tb = gr.Textbox(label="Card lookup", placeholder="e.g., suspious bush") # NEW
|
| 2470 |
+
lookup_btn = gr.Button("Lookup Card Info") # NEW
|
| 2471 |
+
lookup_md = gr.Markdown() # NEW
|
| 2472 |
|
| 2473 |
# ================= Player & Forecast =================
|
| 2474 |
with gr.Tab("Player & Forecast", id=4):
|
|
|
|
| 2551 |
gem_tb.change(_set_gem, [gem_tb], [gem_info])
|
| 2552 |
|
| 2553 |
# --- DECK BUILDER TAB ---
|
| 2554 |
+
def _from_tag_rich(tag, use_gem, micro_w, include_top):
|
|
|
|
| 2555 |
if not tag:
|
| 2556 |
return "Enter a player tag in the sidebar.", None, None, gr.update(visible=False), None
|
| 2557 |
if not STATE.api_token:
|
|
|
|
| 2563 |
p = api.player(t)
|
| 2564 |
except Exception as e:
|
| 2565 |
return f"Error: {e}", None, None, gr.update(visible=False), None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2566 |
|
| 2567 |
+
# NEW: Collect recent matches
|
|
|
|
| 2568 |
recent_matches = collect_recent_matches_for_llm(api, t)
|
| 2569 |
|
| 2570 |
header_lines = player_snapshot_lines(p, t)
|
|
|
|
| 2603 |
use_bracket=True, bracket_center=p.get("trophies"),
|
| 2604 |
player_info_for_pack=player_info_for_pack,
|
| 2605 |
micro_weight=float(micro_w), include_top=bool(include_top),
|
| 2606 |
+
recent_matches_for_pack=recent_matches # Pass new data
|
|
|
|
|
|
|
| 2607 |
)
|
| 2608 |
final_md = "\n".join(header_lines + ["", *perf_lines, "", coach_block])
|
| 2609 |
|
| 2610 |
+
initial_messages = [
|
| 2611 |
+
{"role": "assistant", "content": "Analysis complete. Ask me anything about this deck!"}
|
| 2612 |
+
]
|
| 2613 |
+
initial_pairs = [("", "Analysis complete. Ask me anything about this deck!")]
|
| 2614 |
|
| 2615 |
+
return final_md, pack, initial_messages, gr.update(visible=True), initial_pairs
|
| 2616 |
|
| 2617 |
+
def chat_response(message, messages, pack_state, coach_hist):
|
| 2618 |
+
# messages: [{"role":"user"|"assistant","content":"..."}]
|
| 2619 |
if not pack_state:
|
| 2620 |
+
return messages + [{"role":"assistant","content":"Please analyze a deck first."}], coach_hist
|
|
|
|
| 2621 |
|
| 2622 |
+
# Call your coach with the pair-style history it expects
|
| 2623 |
+
reply = gem_coach(pack_state, history=coach_hist, question=message)
|
| 2624 |
+
|
| 2625 |
+
# Update UI messages
|
| 2626 |
+
new_messages = messages + [
|
| 2627 |
+
{"role":"user", "content": message},
|
| 2628 |
+
{"role":"assistant", "content": reply}
|
| 2629 |
+
]
|
| 2630 |
|
| 2631 |
+
# Update your pair history
|
| 2632 |
+
new_pairs = coach_hist + [(message, reply)]
|
|
|
|
|
|
|
| 2633 |
|
| 2634 |
+
return new_messages, new_pairs
|
| 2635 |
|
|
|
|
| 2636 |
build_from_tag_btn.click(
|
| 2637 |
_from_tag_rich,
|
| 2638 |
+
[player_tag_state, use_gemini, micro_w_slider, include_top_chk],
|
| 2639 |
[builder_md, builder_pack_state, chatbot, chat_interface, coach_history_state]
|
| 2640 |
)
|
| 2641 |
|
|
|
|
| 2677 |
# --- CARD STATS TAB ---
|
| 2678 |
meta_btn.click(recompute_meta, [], [meta_df, meta_plot, wr_plot2])
|
| 2679 |
|
| 2680 |
+
# NEW: Card Lookup handler (grounded search + caching)
|
| 2681 |
def _lookup_card_handler(q):
|
| 2682 |
q = (q or "").strip()
|
| 2683 |
if not q:
|
|
|
|
| 2687 |
info = lookup_card_details(q)
|
| 2688 |
if not info:
|
| 2689 |
return f"Sorry, I couldn't find info for **{q}**."
|
| 2690 |
+
lines = [f"**{info['name']}**"]
|
| 2691 |
if info.get("summary"):
|
| 2692 |
lines.append(info["summary"])
|
| 2693 |
# show basics if we have them
|
|
|
|
| 2698 |
mel = info.get("maxEvolutionLevel", 0)
|
| 2699 |
if mel: bits.append(f"Max Evolution Level: {mel}")
|
| 2700 |
if bits:
|
| 2701 |
+
lines.append("_" + " • ".join(bits) + "_")
|
| 2702 |
# citations
|
| 2703 |
srcs = info.get("sources") or []
|
| 2704 |
if srcs:
|
|
|
|
| 2755 |
|
| 2756 |
|
| 2757 |
if __name__ == "__main__":
|
| 2758 |
+
demo.launch()
|