Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mavera.io/llms.txt

Use this file to discover all available pages before exploring further.

Scenario

Your #product-feedback or #feature-requests channel captures what users actually want — in their own words. This job extracts feature themes from the channel, creates personas representing different user segments, then runs a Mavera Focus Group to pressure-test whether the themes resonate broadly or only with a vocal minority. Flow: Slack conversations.history (#feedback) → Extract themes via Mave → POST /personasPOST /focus-groups → Validated priorities

Code

import os, requests, time

SL_TOKEN = os.environ["SLACK_BOT_TOKEN"]
SL_BASE = "https://slack.com/api"
SL_H = {"Authorization": f"Bearer {SL_TOKEN}"}
MV = os.environ["MAVERA_API_KEY"]
MV_BASE = "https://app.mavera.io/api/v1"
MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

CHANNEL_ID = "C0123FEEDBACK"
DAYS_BACK = 60

# 1. Fetch feedback messages
oldest = str(int(time.time()) - DAYS_BACK * 86400)
messages = []
cursor = None
while True:
    params = {"channel": CHANNEL_ID, "limit": 200, "oldest": oldest}
    if cursor:
        params["cursor"] = cursor
    r = requests.get(f"{SL_BASE}/conversations.history", headers=SL_H, params=params)
    data = r.json()
    if not data.get("ok"):
        break
    messages.extend(data.get("messages", []))
    cursor = data.get("response_metadata", {}).get("next_cursor")
    if not cursor:
        break
    time.sleep(1)

feedback = [m for m in messages if m.get("type") == "message"
            and not m.get("bot_id") and len(m.get("text","")) > 15]
print(f"Feedback input: {len(feedback)}")

# 2. Extract themes via Mave
corpus = "\n".join(f"- {m.get('text','')[:300]}" for m in feedback[-60:])

themes = requests.post(f"{MV_BASE}/mave/chat", headers=MV_H, json={
    "message": f"Product analyst. Extract the top 6 feature themes from {len(feedback)} feedback messages.\n\n"
        f"{corpus[:6000]}\n\n"
        "For each theme: Name (3-5 words), frequency (count of mentions), "
        "representative quote, user impact (1-10).\n"
        "Return as a numbered list."
}).json()
themes_text = themes.get("content", "")
print(f"\nThemes:\n{themes_text[:1000]}")

# 3. Create user-segment personas
SEGMENTS = [
    {"name": "Power User (Daily)", "desc": "Uses the product 4+ hours/day. Has workarounds for missing features. Values efficiency and keyboard shortcuts."},
    {"name": "Casual User (Weekly)", "desc": "Uses the product 2-3 times per week. Needs simplicity. Gets frustrated by complexity. Values intuitive UX."},
    {"name": "Team Admin", "desc": "Manages 20+ team members on the platform. Cares about permissions, reporting, onboarding new users. Values admin controls."},
    {"name": "New User (< 30 days)", "desc": "Just started using the product. Still learning. Compares to previous tools. Values onboarding and documentation."},
]
persona_ids = []
for seg in SEGMENTS:
    p = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
        "name": f"Feedback: {seg['name']}", "description": seg["desc"],
    }).json()
    persona_ids.append(p["id"])
    time.sleep(0.3)

# 4. Focus Group to validate themes
fg = requests.post(f"{MV_BASE}/focus-groups", headers=MV_H, json={
    "name": "Product Feedback Validation",
    "persona_ids": persona_ids,
    "questions": [
        f"Here are the top feature requests from our users:\n{themes_text[:1500]}\n\nWhich of these would make the biggest difference for YOU?",
        "Is there a feature NOT on this list that you need more urgently?",
        "If we could only ship ONE of these this quarter, which should it be and why?",
        "Would any of these features make you upgrade to a higher plan?",
    ],
    "responses_per_persona": 2,
}).json()

for _ in range(25):
    time.sleep(5)
    data = requests.get(f"{MV_BASE}/focus-groups/{fg['id']}", headers=MV_H).json()
    if data.get("status") == "completed":
        break

print(f"\n{'='*60}\nFOCUS GROUP VALIDATION\n{'='*60}")
for resp in data.get("responses", []):
    idx = persona_ids.index(resp.get("persona_id")) if resp.get("persona_id") in persona_ids else -1
    name = SEGMENTS[idx]["name"] if 0 <= idx < len(SEGMENTS) else "Unknown"
    print(f"\n[{name}] Q: {resp.get('question','')[:80]}")
    print(f"  A: {resp.get('answer','')[:350]}")

Example Output

Feedback messages: 187

Themes:
1. Dark Mode (28 mentions, impact: 6/10)
2. API Webhooks (22 mentions, impact: 8/10)
3. Better Mobile App (19 mentions, impact: 7/10)
4. Custom Dashboards (15 mentions, impact: 9/10)
5. SSO for Teams (12 mentions, impact: 8/10)
6. Bulk Export (9 mentions, impact: 5/10)

FOCUS GROUP VALIDATION
============================================================
[Power User] Q: Ship ONE this quarter?
  A: API Webhooks. Dark mode is cosmetic. I need webhooks to automate
     my workflow — currently polling every 5 minutes which is brittle.
     Webhooks unlock an entire integration ecosystem.

[New User] Q: Ship ONE this quarter?
  A: Better Mobile App. I'm still learning the desktop version and
     the mobile app feels like a different product. If onboarding
     was mobile-first, I'd use it way more.

[Team Admin] Q: Would any make you upgrade?
  A: Custom Dashboards. I currently export to Google Sheets to build
     reports for leadership. Native dashboards = immediate upgrade.

Error Handling

Channels with fewer than 20 messages in 60 days may not produce meaningful themes. Extend DAYS_BACK or combine with related channels.
Slack reactions (thumbsup, fire, etc.) indicate agreement. Count reactions on feedback messages to weight importance. Access via the reactions field in message objects.
Complex questions with 4+ personas may take 60-90 seconds. The polling loop allows up to 125 seconds.