Spaces:
Running
Running
Commit ·
77d1323
1
Parent(s): fc2a790
Fix HF debate arena fallbacks, score focus 0 in LLM mode, and typewriter concurrent race condition
Browse files- app/engine.py +124 -0
- app/static/app.js +13 -3
app/engine.py
CHANGED
|
@@ -575,6 +575,29 @@ def extract_aspect_argument(persona, item, aspect, query, memory_pool=None):
|
|
| 575 |
text = pattern.sub(item["title"], text)
|
| 576 |
text = re.sub(r'\s+', ' ', text).strip()
|
| 577 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
return text
|
| 579 |
|
| 580 |
|
|
@@ -829,6 +852,93 @@ def _calibrate_agent_score(raw_score_0_100, item_avg_rating=4.0):
|
|
| 829 |
calibrated = item_avg_rating + deviation * 2.0
|
| 830 |
return max(1.0, min(5.0, round(calibrated, 2)))
|
| 831 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 832 |
# ----------------------------------------------------------------------
|
| 833 |
# 5. LOSS & WEIGHTS PARAMETER TRAINING (COORDINATE DESCENT)
|
| 834 |
# ----------------------------------------------------------------------
|
|
@@ -2017,6 +2127,7 @@ class TasteTwinEngine:
|
|
| 2017 |
parsed_debate = []
|
| 2018 |
if debate_match:
|
| 2019 |
parsed_debate = parse_debate_transcript(debate_match.group(1))
|
|
|
|
| 2020 |
|
| 2021 |
return {
|
| 2022 |
"rating": rating,
|
|
@@ -2177,6 +2288,8 @@ class TasteTwinEngine:
|
|
| 2177 |
r["price"] = real_item["price"]
|
| 2178 |
r["currency"] = real_item["currency"]
|
| 2179 |
r["category"] = real_item["category"]
|
|
|
|
|
|
|
| 2180 |
valid_recs.append(r)
|
| 2181 |
else:
|
| 2182 |
# Fallback matching by title
|
|
@@ -2190,12 +2303,23 @@ class TasteTwinEngine:
|
|
| 2190 |
r["price"] = real_item["price"]
|
| 2191 |
r["currency"] = real_item["currency"]
|
| 2192 |
r["category"] = real_item["category"]
|
|
|
|
|
|
|
| 2193 |
valid_recs.append(r)
|
| 2194 |
else:
|
| 2195 |
# Default values so UI doesn't crash or show N/A
|
| 2196 |
r["price"] = r.get("price") or 15000.0
|
| 2197 |
r["currency"] = r.get("currency") or "NGN"
|
| 2198 |
r["category"] = r.get("category") or "electronics"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2199 |
valid_recs.append(r)
|
| 2200 |
|
| 2201 |
valid_recs.sort(key=lambda x: x.get("predicted_rating", 0.0), reverse=True)
|
|
|
|
| 575 |
text = pattern.sub(item["title"], text)
|
| 576 |
text = re.sub(r'\s+', ' ', text).strip()
|
| 577 |
|
| 578 |
+
# Beautifully synthesize natural fallbacks when RAG matches raw metadata context strings
|
| 579 |
+
text_lower = text.lower()
|
| 580 |
+
if "target product features:" in text_lower or "category affinities:" in text_lower or "historical aspect sentiments:" in text_lower or "no specific historical traces for taste" in text_lower:
|
| 581 |
+
features_list = ", ".join(item.get("features", []))
|
| 582 |
+
text = f"Looking closely at the specifications for {item['title']}, the build features are quite compelling: {features_list}. For a user with {persona['name']}'s distinct taste DNA, this matches extremely well omo!"
|
| 583 |
+
elif "target product price:" in text_lower or "budget sensitivity dna:" in text_lower or "historical prices paid:" in text_lower or "average historical price:" in text_lower or "no specific historical traces for budget" in text_lower:
|
| 584 |
+
text = f"Let's talk about the pricing for {item['title']}. It is listed at {item['price']} {item['currency']}. Considering {persona['name']}'s budget sensitivity DNA of {persona['dna']['budget']}%, this price point requires careful value assessment to ensure complete alignment sha."
|
| 585 |
+
elif "is this category new to the user" in text_lower or "novelty exploration dna:" in text_lower or "historically purchased categories:" in text_lower or "target product category:" in text_lower or "no specific historical traces for novelty" in text_lower:
|
| 586 |
+
user_cats = set(r["category"] for r in persona.get("history", []))
|
| 587 |
+
is_new = item["category"] not in user_cats
|
| 588 |
+
if is_new:
|
| 589 |
+
text = f"This category is actually completely new to {persona['name']}! Since their novelty exploration DNA is set to {persona['dna']['novelty']}%, this is an exciting opportunity to try something completely fresh, abeg!"
|
| 590 |
+
else:
|
| 591 |
+
text = f"This category is familiar to {persona['name']}. But even within a familiar domain, the unique qualities of {item['title']} should offer a nice, satisfying twist to their routine."
|
| 592 |
+
elif "persona recent mood state:" in text_lower or "strictness dna:" in text_lower or "historical rating variance:" in text_lower or "exclamations across" in text_lower or "no specific historical traces for mood" in text_lower:
|
| 593 |
+
mood = persona.get("recent_mood", "standard")
|
| 594 |
+
text = f"Looking at {persona['name']}'s current mood, they are feeling {mood}. With a strictness level of {persona['dna']['strictness']}%, their expectations will be quite high omo, so there is no room for subpar performance."
|
| 595 |
+
elif "nigerian behavioral cues to prioritize:" in text_lower or "nigerian affinity scale dna:" in text_lower or "no specific historical traces for cultural" in text_lower:
|
| 596 |
+
text = f"For the Nigerian context, durability is key. We are looking at durability under NEPA grid fluctuations, resale value, and Lagos traffic/heavy rain delivery logistics. {item['title']} needs to show it can survive the local elements sha."
|
| 597 |
+
elif "target product complaints:" in text_lower or "no specific historical traces for post_consumption" in text_lower:
|
| 598 |
+
complaints_list = ", ".join(item.get("complaints", [])) if item.get("complaints") else "no major complaints reported"
|
| 599 |
+
text = f"I noticed some minor complaints about {item['title']}: {complaints_list}. While these shouldn't be dealbreakers, they are worth keeping in mind for the overall user experience."
|
| 600 |
+
|
| 601 |
return text
|
| 602 |
|
| 603 |
|
|
|
|
| 852 |
calibrated = item_avg_rating + deviation * 2.0
|
| 853 |
return max(1.0, min(5.0, round(calibrated, 2)))
|
| 854 |
|
| 855 |
+
|
| 856 |
+
def inject_mathematical_agent_scores(persona, item, debate_script, final_rating=None):
|
| 857 |
+
"""
|
| 858 |
+
Dynamically overrides/injects actual mathematically optimized DNA focus scores
|
| 859 |
+
into each debate agent's turn to prevent flat 0.0 scores in LLM outputs.
|
| 860 |
+
"""
|
| 861 |
+
if not debate_script:
|
| 862 |
+
return debate_script
|
| 863 |
+
|
| 864 |
+
item_avg = item.get("avg_rating", 4.0) if item else 4.0
|
| 865 |
+
|
| 866 |
+
# 1. Taste Score
|
| 867 |
+
taste_score = item_avg * 20.0
|
| 868 |
+
if item and item["category"] == persona["domain"]:
|
| 869 |
+
taste_score += 10.0
|
| 870 |
+
cal_taste = _calibrate_agent_score(taste_score, item_avg)
|
| 871 |
+
|
| 872 |
+
# 2. Budget Score
|
| 873 |
+
price_ngn = 15000.0
|
| 874 |
+
if item:
|
| 875 |
+
price_ngn = item["price"] if item["currency"] == "NGN" else item["price"] * 1500.0
|
| 876 |
+
budget_sensitivity = persona["dna"]["budget"]
|
| 877 |
+
if price_ngn > 100000:
|
| 878 |
+
budget_score = max(10.0, 100.0 - (budget_sensitivity * 0.9))
|
| 879 |
+
elif price_ngn > 30000:
|
| 880 |
+
budget_score = max(30.0, 100.0 - (budget_sensitivity * 0.5))
|
| 881 |
+
else:
|
| 882 |
+
budget_score = min(100.0, 50.0 + (budget_sensitivity * 0.5))
|
| 883 |
+
cal_budget = _calibrate_agent_score(budget_score, item_avg)
|
| 884 |
+
|
| 885 |
+
# 3. Novelty Score
|
| 886 |
+
user_cats = set(rec["category"] for rec in persona.get("history", []))
|
| 887 |
+
novelty_sensitivity = persona["dna"]["novelty"]
|
| 888 |
+
if item and item["category"] not in user_cats:
|
| 889 |
+
novelty_score = novelty_sensitivity
|
| 890 |
+
else:
|
| 891 |
+
novelty_score = 100.0 - (novelty_sensitivity * 0.5)
|
| 892 |
+
cal_novelty = _calibrate_agent_score(novelty_score, item_avg)
|
| 893 |
+
|
| 894 |
+
# 4. Mood Score
|
| 895 |
+
mood = persona.get("recent_mood", "standard")
|
| 896 |
+
mood_score = 70.0
|
| 897 |
+
if mood == "frustrated":
|
| 898 |
+
mood_score = 40.0
|
| 899 |
+
elif mood == "happy":
|
| 900 |
+
mood_score = 90.0
|
| 901 |
+
cal_mood = _calibrate_agent_score(mood_score, item_avg)
|
| 902 |
+
|
| 903 |
+
# 5. Cultural Score
|
| 904 |
+
naija_scale = persona["dna"]["naija_scale"]
|
| 905 |
+
naija_score = 70.0
|
| 906 |
+
if item:
|
| 907 |
+
has_delivery_issue = any("delivery" in c.lower() or "wait" in c.lower() or "delay" in c.lower() for c in item.get("complaints", []))
|
| 908 |
+
has_power_issue = any("charge" in c.lower() or "nepa" in c.lower() or "battery" in c.lower() or "power" in c.lower() for c in item.get("complaints", []))
|
| 909 |
+
has_repair_issue = any("broken" in c.lower() or "fragile" in c.lower() or "break" in c.lower() for c in item.get("complaints", []))
|
| 910 |
+
|
| 911 |
+
if naija_scale > 60:
|
| 912 |
+
if has_delivery_issue:
|
| 913 |
+
naija_score -= 25.0
|
| 914 |
+
if has_power_issue:
|
| 915 |
+
naija_score -= 20.0
|
| 916 |
+
if has_repair_issue:
|
| 917 |
+
naija_score -= 15.0
|
| 918 |
+
cal_cultural = _calibrate_agent_score(naija_score, item_avg)
|
| 919 |
+
|
| 920 |
+
for step in debate_script:
|
| 921 |
+
if not isinstance(step, dict):
|
| 922 |
+
continue
|
| 923 |
+
agent_name = step.get("agent", "")
|
| 924 |
+
if "Taste" in agent_name:
|
| 925 |
+
step["score"] = cal_taste
|
| 926 |
+
elif "Budget" in agent_name:
|
| 927 |
+
step["score"] = cal_budget
|
| 928 |
+
elif "Novelty" in agent_name:
|
| 929 |
+
step["score"] = cal_novelty
|
| 930 |
+
elif "Mood" in agent_name:
|
| 931 |
+
step["score"] = cal_mood
|
| 932 |
+
elif "Cultural" in agent_name:
|
| 933 |
+
step["score"] = cal_cultural
|
| 934 |
+
elif "Judge" in agent_name:
|
| 935 |
+
if final_rating is not None:
|
| 936 |
+
step["score"] = final_rating
|
| 937 |
+
elif step.get("score") == 0.0 or not step.get("score"):
|
| 938 |
+
step["score"] = item_avg
|
| 939 |
+
|
| 940 |
+
return debate_script
|
| 941 |
+
|
| 942 |
# ----------------------------------------------------------------------
|
| 943 |
# 5. LOSS & WEIGHTS PARAMETER TRAINING (COORDINATE DESCENT)
|
| 944 |
# ----------------------------------------------------------------------
|
|
|
|
| 2127 |
parsed_debate = []
|
| 2128 |
if debate_match:
|
| 2129 |
parsed_debate = parse_debate_transcript(debate_match.group(1))
|
| 2130 |
+
parsed_debate = inject_mathematical_agent_scores(persona, item, parsed_debate, final_rating=rating)
|
| 2131 |
|
| 2132 |
return {
|
| 2133 |
"rating": rating,
|
|
|
|
| 2288 |
r["price"] = real_item["price"]
|
| 2289 |
r["currency"] = real_item["currency"]
|
| 2290 |
r["category"] = real_item["category"]
|
| 2291 |
+
if "debate" in r and isinstance(r["debate"], list):
|
| 2292 |
+
r["debate"] = inject_mathematical_agent_scores(persona, real_item, r["debate"], final_rating=r.get("predicted_rating"))
|
| 2293 |
valid_recs.append(r)
|
| 2294 |
else:
|
| 2295 |
# Fallback matching by title
|
|
|
|
| 2303 |
r["price"] = real_item["price"]
|
| 2304 |
r["currency"] = real_item["currency"]
|
| 2305 |
r["category"] = real_item["category"]
|
| 2306 |
+
if "debate" in r and isinstance(r["debate"], list):
|
| 2307 |
+
r["debate"] = inject_mathematical_agent_scores(persona, real_item, r["debate"], final_rating=r.get("predicted_rating"))
|
| 2308 |
valid_recs.append(r)
|
| 2309 |
else:
|
| 2310 |
# Default values so UI doesn't crash or show N/A
|
| 2311 |
r["price"] = r.get("price") or 15000.0
|
| 2312 |
r["currency"] = r.get("currency") or "NGN"
|
| 2313 |
r["category"] = r.get("category") or "electronics"
|
| 2314 |
+
dummy_item = {
|
| 2315 |
+
"id": r.get("item_id", "dummy"),
|
| 2316 |
+
"title": r.get("title", "Product"),
|
| 2317 |
+
"category": r["category"],
|
| 2318 |
+
"price": r["price"],
|
| 2319 |
+
"currency": r["currency"]
|
| 2320 |
+
}
|
| 2321 |
+
if "debate" in r and isinstance(r["debate"], list):
|
| 2322 |
+
r["debate"] = inject_mathematical_agent_scores(persona, dummy_item, r["debate"], final_rating=r.get("predicted_rating"))
|
| 2323 |
valid_recs.append(r)
|
| 2324 |
|
| 2325 |
valid_recs.sort(key=lambda x: x.get("predicted_rating", 0.0), reverse=True)
|
app/static/app.js
CHANGED
|
@@ -553,12 +553,13 @@ function playSimulationResult(monologue, review, rating) {
|
|
| 553 |
});
|
| 554 |
}
|
| 555 |
|
| 556 |
-
function typeWriter(element, text, speed = 20, callback = null) {
|
| 557 |
let index = 0;
|
| 558 |
// Fast typing: do word by word or chunk by chunk if long text to avoid user frustration
|
| 559 |
let words = text.split(" ");
|
| 560 |
|
| 561 |
function type() {
|
|
|
|
| 562 |
if (index < words.length) {
|
| 563 |
element.textContent += (index === 0 ? "" : " ") + words[index];
|
| 564 |
index++;
|
|
@@ -848,13 +849,17 @@ function selectDebateItem(itemId) {
|
|
| 848 |
}
|
| 849 |
}
|
| 850 |
|
|
|
|
|
|
|
| 851 |
function playAgentDebateSequence(debate) {
|
| 852 |
const arena = document.getElementById("debate-arena");
|
| 853 |
arena.innerHTML = "";
|
| 854 |
|
|
|
|
| 855 |
let index = 0;
|
| 856 |
|
| 857 |
function playNextBubble() {
|
|
|
|
| 858 |
if (index < debate.length) {
|
| 859 |
const step = debate[index];
|
| 860 |
const bubble = document.createElement("div");
|
|
@@ -877,9 +882,14 @@ function playAgentDebateSequence(debate) {
|
|
| 877 |
// Type message
|
| 878 |
const msgContainer = document.getElementById(`debate-msg-${index}`);
|
| 879 |
typeWriter(msgContainer, step.text, 15, () => {
|
|
|
|
| 880 |
index++;
|
| 881 |
-
setTimeout(
|
| 882 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 883 |
}
|
| 884 |
}
|
| 885 |
|
|
|
|
| 553 |
});
|
| 554 |
}
|
| 555 |
|
| 556 |
+
function typeWriter(element, text, speed = 20, callback = null, playIdToCheck = null) {
|
| 557 |
let index = 0;
|
| 558 |
// Fast typing: do word by word or chunk by chunk if long text to avoid user frustration
|
| 559 |
let words = text.split(" ");
|
| 560 |
|
| 561 |
function type() {
|
| 562 |
+
if (playIdToCheck !== null && playIdToCheck !== currentDebatePlayId) return;
|
| 563 |
if (index < words.length) {
|
| 564 |
element.textContent += (index === 0 ? "" : " ") + words[index];
|
| 565 |
index++;
|
|
|
|
| 849 |
}
|
| 850 |
}
|
| 851 |
|
| 852 |
+
let currentDebatePlayId = 0;
|
| 853 |
+
|
| 854 |
function playAgentDebateSequence(debate) {
|
| 855 |
const arena = document.getElementById("debate-arena");
|
| 856 |
arena.innerHTML = "";
|
| 857 |
|
| 858 |
+
const playId = ++currentDebatePlayId;
|
| 859 |
let index = 0;
|
| 860 |
|
| 861 |
function playNextBubble() {
|
| 862 |
+
if (playId !== currentDebatePlayId) return;
|
| 863 |
if (index < debate.length) {
|
| 864 |
const step = debate[index];
|
| 865 |
const bubble = document.createElement("div");
|
|
|
|
| 882 |
// Type message
|
| 883 |
const msgContainer = document.getElementById(`debate-msg-${index}`);
|
| 884 |
typeWriter(msgContainer, step.text, 15, () => {
|
| 885 |
+
if (playId !== currentDebatePlayId) return;
|
| 886 |
index++;
|
| 887 |
+
setTimeout(() => {
|
| 888 |
+
if (playId === currentDebatePlayId) {
|
| 889 |
+
playNextBubble();
|
| 890 |
+
}
|
| 891 |
+
}, 600); // delay before next agent enters
|
| 892 |
+
}, playId);
|
| 893 |
}
|
| 894 |
}
|
| 895 |
|