> ## 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.

# Product Feedback → Focus Group

### 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 /personas` → `POST /focus-groups` → Validated priorities

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  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]}")
  ```

  ```javascript JavaScript theme={"dark"}
  const SL_TOKEN = process.env.SLACK_BOT_TOKEN;
  const SL_BASE = "https://slack.com/api";
  const SL_H = { Authorization: `Bearer ${SL_TOKEN}` };
  const MV = process.env.MAVERA_API_KEY;
  const MV_BASE = "https://app.mavera.io/api/v1";
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const CHANNEL_ID = "C0123FEEDBACK";
  const DAYS_BACK = 60;
  const oldest = String(Math.floor(Date.now() / 1000) - DAYS_BACK * 86400);

  // 1. Fetch
  const messages = [];
  let cursor;
  do {
    const params = new URLSearchParams({ channel: CHANNEL_ID, limit: "200", oldest });
    if (cursor) params.set("cursor", cursor);
    const data = await (await fetch(`${SL_BASE}/conversations.history?${params}`, { headers: SL_H })).json();
    if (!data.ok) break;
    messages.push(...(data.messages || []));
    cursor = data.response_metadata?.next_cursor;
    await new Promise(r => setTimeout(r, 1000));
  } while (cursor);

  const feedback = messages.filter(m => m.type === "message" && !m.bot_id && (m.text||"").length > 15);
  console.log(`Feedback messages: ${feedback.length}`);

  // 2. Themes
  const corpus = feedback.slice(-60).map(m => `- ${(m.text||"").slice(0,300)}`).join("\n");
  const themes = await fetch(`${MV_BASE}/mave/chat`, { method: "POST", headers: MV_H,
    body: JSON.stringify({ message: `Top 6 feature themes from ${feedback.length} messages.\n\n${corpus.slice(0,6000)}\n\nPer theme: name, frequency, quote, impact (1-10). Numbered list.` }),
  }).then(r => r.json());
  const themesText = themes.content || "";
  console.log(`Themes:\n${themesText.slice(0, 1000)}`);

  // 3. Personas
  const SEGMENTS = [
    { name: "Power User (Daily)", desc: "4+ hours/day. Workarounds. Values efficiency." },
    { name: "Casual User (Weekly)", desc: "2-3x/week. Needs simplicity. Intuitive UX." },
    { name: "Team Admin", desc: "Manages 20+ members. Permissions, reporting." },
    { name: "New User (< 30 days)", desc: "Just started. Learning. Compares to previous tools." },
  ];
  const personaIds = [];
  for (const seg of SEGMENTS) {
    const p = await fetch(`${MV_BASE}/personas`, { method: "POST", headers: MV_H,
      body: JSON.stringify({ name: `Feedback: ${seg.name}`, description: seg.desc }),
    }).then(r => r.json());
    personaIds.push(p.id);
    await new Promise(r => setTimeout(r, 300));
  }

  // 4. Focus Group
  const fg = await fetch(`${MV_BASE}/focus-groups`, { method: "POST", headers: MV_H,
    body: JSON.stringify({ name: "Product Feedback Validation", persona_ids: personaIds,
      questions: [
        `Top requests:\n${themesText.slice(0,1500)}\n\nBiggest difference for YOU?`,
        "Feature NOT listed that you need more?",
        "Ship ONE this quarter — which and why?",
        "Would any make you upgrade plans?",
      ], responses_per_persona: 2 }),
  }).then(r => r.json());

  let data;
  for (let i = 0; i < 25; i++) {
    await new Promise(r => setTimeout(r, 5000));
    data = await fetch(`${MV_BASE}/focus-groups/${fg.id}`, { headers: MV_H }).then(r => r.json());
    if (data.status === "completed") break;
  }

  console.log(`\n${"=".repeat(60)}\nFOCUS GROUP VALIDATION`);
  for (const resp of data.responses || []) {
    const idx = personaIds.indexOf(resp.persona_id);
    const name = idx >= 0 ? SEGMENTS[idx].name : "Unknown";
    console.log(`\n[${name}] Q: ${(resp.question||"").slice(0,80)}`);
    console.log(`  A: ${(resp.answer||"").slice(0,350)}`);
  }
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
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

<AccordionGroup>
  <Accordion title="Low feedback volume">Channels with fewer than 20 messages in 60 days may not produce meaningful themes. Extend `DAYS_BACK` or combine with related channels.</Accordion>
  <Accordion title="Emoji reactions as data">Slack reactions (thumbsup, fire, etc.) indicate agreement. Count reactions on feedback messages to weight importance. Access via the `reactions` field in message objects.</Accordion>
  <Accordion title="Focus Group timeout">Complex questions with 4+ personas may take 60-90 seconds. The polling loop allows up to 125 seconds.</Accordion>
</AccordionGroup>
