Eric Xu commited on
Auto-suggest context-aware segments from entity via LLM
Browse filesThe cohort step now calls the LLM to suggest segments that match the
entity domain (dating profile → partner types, product → buyer personas,
resume → hiring managers). Replaces hardcoded product-focused defaults.
- Add /api/suggest-segments endpoint
- Auto-call on step 2 entry, with Re-suggest button for manual refresh
- web/app.py +46 -0
- web/static/index.html +35 -10
web/app.py
CHANGED
|
@@ -88,6 +88,11 @@ class CounterfactualConfig(BaseModel):
|
|
| 88 |
parallel: int = 5
|
| 89 |
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
# ── Routes ────────────────────────────────────────────────────────────────
|
| 92 |
|
| 93 |
@app.get("/")
|
|
@@ -134,6 +139,47 @@ async def get_session(sid: str):
|
|
| 134 |
}
|
| 135 |
|
| 136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
@app.post("/api/cohort/generate")
|
| 138 |
async def generate_cohort_endpoint(config: CohortConfig):
|
| 139 |
"""Generate an LLM cohort and attach to a new session."""
|
|
|
|
| 88 |
parallel: int = 5
|
| 89 |
|
| 90 |
|
| 91 |
+
class SuggestSegmentsInput(BaseModel):
|
| 92 |
+
entity_text: str
|
| 93 |
+
audience_context: str
|
| 94 |
+
|
| 95 |
+
|
| 96 |
# ── Routes ────────────────────────────────────────────────────────────────
|
| 97 |
|
| 98 |
@app.get("/")
|
|
|
|
| 139 |
}
|
| 140 |
|
| 141 |
|
| 142 |
+
@app.post("/api/suggest-segments")
|
| 143 |
+
async def suggest_segments(input: SuggestSegmentsInput):
|
| 144 |
+
"""Use LLM to suggest audience segments based on entity and context."""
|
| 145 |
+
client = get_client()
|
| 146 |
+
model = get_model()
|
| 147 |
+
|
| 148 |
+
prompt = f"""Given this entity and audience context, suggest 4-5 evaluator segments.
|
| 149 |
+
Each segment should represent a distinct perspective that would evaluate this entity differently.
|
| 150 |
+
|
| 151 |
+
Entity:
|
| 152 |
+
{input.entity_text[:2000]}
|
| 153 |
+
|
| 154 |
+
Audience context: {input.audience_context}
|
| 155 |
+
|
| 156 |
+
Return JSON:
|
| 157 |
+
{{
|
| 158 |
+
"segments": [
|
| 159 |
+
{{"label": "<concise segment description, 5-10 words>", "count": <6-10>}}
|
| 160 |
+
]
|
| 161 |
+
}}
|
| 162 |
+
|
| 163 |
+
Make segments specific to THIS domain. For a product, use buyer personas. For a dating profile,
|
| 164 |
+
use different types of potential partners. For a resume, use different hiring managers. Etc.
|
| 165 |
+
Be concrete and relevant — no generic segments."""
|
| 166 |
+
|
| 167 |
+
try:
|
| 168 |
+
resp = client.chat.completions.create(
|
| 169 |
+
model=model,
|
| 170 |
+
messages=[{"role": "user", "content": prompt}],
|
| 171 |
+
response_format={"type": "json_object"},
|
| 172 |
+
max_tokens=1024,
|
| 173 |
+
temperature=0.7,
|
| 174 |
+
)
|
| 175 |
+
content = resp.choices[0].message.content
|
| 176 |
+
content = re.sub(r'<think>[\s\S]*?</think>', '', content).strip()
|
| 177 |
+
data = json.loads(content)
|
| 178 |
+
return data
|
| 179 |
+
except Exception as e:
|
| 180 |
+
raise HTTPException(500, f"Failed to suggest segments: {e}")
|
| 181 |
+
|
| 182 |
+
|
| 183 |
@app.post("/api/cohort/generate")
|
| 184 |
async def generate_cohort_endpoint(config: CohortConfig):
|
| 185 |
"""Generate an LLM cohort and attach to a new session."""
|
web/static/index.html
CHANGED
|
@@ -347,6 +347,7 @@
|
|
| 347 |
<div id="segmentsList" class="segments-list"></div>
|
| 348 |
<div class="btn-row">
|
| 349 |
<button class="secondary" onclick="addSegment()">+ Add segment</button>
|
|
|
|
| 350 |
<button onclick="generateCohort()" id="genCohortBtn">Generate cohort</button>
|
| 351 |
</div>
|
| 352 |
|
|
@@ -593,16 +594,9 @@ async function init() {
|
|
| 593 |
badge.className = 'config-badge warn';
|
| 594 |
}
|
| 595 |
|
| 596 |
-
//
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
addSegment('Budget-conscious comparison shopper', 8);
|
| 600 |
-
addSegment('Enterprise decision-maker', 8);
|
| 601 |
-
|
| 602 |
-
// Default changes
|
| 603 |
-
addChange('Add free tier', 'Introduce a generous free plan that lets users try core features with no credit card required.');
|
| 604 |
-
addChange('Add social proof', 'Display customer logos, case studies, and specific metrics (e.g., "Used by 5,000 teams") prominently on the page.');
|
| 605 |
-
addChange('Lower price by 40%', 'Reduce all paid plan prices by 40% across the board.');
|
| 606 |
}
|
| 607 |
|
| 608 |
// ── Templates ──
|
|
@@ -656,6 +650,37 @@ async function saveEntity() {
|
|
| 656 |
if (!desc.value) desc.value = `People evaluating: ${firstLine.replace(/^#+\s*/, '').trim()}`;
|
| 657 |
|
| 658 |
goToStep(2);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
}
|
| 660 |
|
| 661 |
// ── Step 2: Cohort ──
|
|
|
|
| 347 |
<div id="segmentsList" class="segments-list"></div>
|
| 348 |
<div class="btn-row">
|
| 349 |
<button class="secondary" onclick="addSegment()">+ Add segment</button>
|
| 350 |
+
<button class="secondary" onclick="suggestSegments()">Re-suggest</button>
|
| 351 |
<button onclick="generateCohort()" id="genCohortBtn">Generate cohort</button>
|
| 352 |
</div>
|
| 353 |
|
|
|
|
| 594 |
badge.className = 'config-badge warn';
|
| 595 |
}
|
| 596 |
|
| 597 |
+
// Changes will be populated with defaults
|
| 598 |
+
addChange('', '');
|
| 599 |
+
addChange('', '');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 600 |
}
|
| 601 |
|
| 602 |
// ── Templates ──
|
|
|
|
| 650 |
if (!desc.value) desc.value = `People evaluating: ${firstLine.replace(/^#+\s*/, '').trim()}`;
|
| 651 |
|
| 652 |
goToStep(2);
|
| 653 |
+
|
| 654 |
+
// Auto-suggest segments via LLM
|
| 655 |
+
await suggestSegments();
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
async function suggestSegments() {
|
| 659 |
+
const entityText = document.getElementById('entityText').value.trim();
|
| 660 |
+
const audienceCtx = document.getElementById('cohortDesc').value.trim();
|
| 661 |
+
if (!entityText) return;
|
| 662 |
+
|
| 663 |
+
// Clear existing segments and show loading
|
| 664 |
+
const list = document.getElementById('segmentsList');
|
| 665 |
+
list.innerHTML = '<div style="color:var(--text2);font-size:0.85rem;padding:8px">Suggesting segments...</div>';
|
| 666 |
+
|
| 667 |
+
try {
|
| 668 |
+
const resp = await fetch('/api/suggest-segments', {
|
| 669 |
+
method: 'POST',
|
| 670 |
+
headers: {'Content-Type': 'application/json'},
|
| 671 |
+
body: JSON.stringify({entity_text: entityText, audience_context: audienceCtx}),
|
| 672 |
+
});
|
| 673 |
+
const data = await resp.json();
|
| 674 |
+
list.innerHTML = '';
|
| 675 |
+
(data.segments || []).forEach(seg => {
|
| 676 |
+
addSegment(seg.label, seg.count || 8);
|
| 677 |
+
});
|
| 678 |
+
} catch (e) {
|
| 679 |
+
// Fallback to empty segments on error
|
| 680 |
+
list.innerHTML = '';
|
| 681 |
+
addSegment('', 8);
|
| 682 |
+
addSegment('', 8);
|
| 683 |
+
}
|
| 684 |
}
|
| 685 |
|
| 686 |
// ── Step 2: Cohort ──
|