Eric Xu commited on
Commit
61872af
·
unverified ·
1 Parent(s): a81335f

Extract demographic filters from audience context for Nemotron cohorts

Browse files

The Nemotron path was ignoring audience context entirely (empty filters),
returning random Americans regardless of what the user specified. Now the
LLM extracts structured filters (sex, age range, city, state, education,
occupation) from the audience context and passes them to filter_personas.

"Women in Chicago aged 25-45" → {sex: Female, city: Chicago, state: IL,
age_min: 25, age_max: 45} → only matching Nemotron personas are sampled.

Extracted filters are shown in the progress log for transparency.

Files changed (2) hide show
  1. web/app.py +54 -1
  2. web/static/index.html +11 -1
web/app.py CHANGED
@@ -131,6 +131,7 @@ class EntityInput(BaseModel):
131
 
132
  class CohortConfig(BaseModel):
133
  description: str
 
134
  segments: list[dict] # [{"label": "...", "count": N}, ...]
135
  parallel: int = 3
136
 
@@ -318,6 +319,50 @@ Be concrete and relevant — no generic segments."""
318
  raise HTTPException(500, f"Failed to suggest segments: {e}")
319
 
320
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  @app.post("/api/cohort/generate")
322
  async def generate_cohort_endpoint(config: CohortConfig):
323
  """Generate a cohort — from Nemotron if available, else LLM-generated."""
@@ -330,7 +375,14 @@ async def generate_cohort_endpoint(config: CohortConfig):
330
  import random
331
  pl = _lazy_persona_loader()
332
  ss = _lazy_stratified_sampler()
333
- filtered = pl.filter_personas(ds, {}, limit=max(total * 20, 2000))
 
 
 
 
 
 
 
334
  profiles = [pl.to_profile(row, i) for i, row in enumerate(filtered)]
335
 
336
  # Use only age + education to keep strata count < total
@@ -379,6 +431,7 @@ async def generate_cohort_endpoint(config: CohortConfig):
379
  return {
380
  "session_id": sid, "cohort_size": len(all_personas),
381
  "cohort": all_personas, "source": source,
 
382
  }
383
 
384
 
 
131
 
132
  class CohortConfig(BaseModel):
133
  description: str
134
+ audience_context: str = ""
135
  segments: list[dict] # [{"label": "...", "count": N}, ...]
136
  parallel: int = 3
137
 
 
319
  raise HTTPException(500, f"Failed to suggest segments: {e}")
320
 
321
 
322
+ def extract_filters(client, model, audience_context, entity_text=""):
323
+ """Use LLM to extract structured Nemotron filters from audience context."""
324
+ if not audience_context.strip():
325
+ return {}
326
+
327
+ prompt = f"""Extract structured demographic filters from this audience description.
328
+ Only include filters that are explicitly stated or clearly implied.
329
+
330
+ Audience: {audience_context}
331
+ Entity context: {entity_text[:500]}
332
+
333
+ Return JSON with ONLY the fields that apply (omit fields that aren't specified):
334
+ {{
335
+ "sex": "Male" or "Female",
336
+ "age_min": <number>,
337
+ "age_max": <number>,
338
+ "state": "<2-letter state code, e.g. IL for Illinois>",
339
+ "city": "<city name substring>",
340
+ "education_level": ["bachelors", "graduate", ...],
341
+ "occupation": "<occupation substring>"
342
+ }}
343
+
344
+ If the audience is "women in Chicago aged 25-35", return:
345
+ {{"sex": "Female", "city": "Chicago", "state": "IL", "age_min": 25, "age_max": 35}}
346
+
347
+ If nothing specific is stated, return {{}}."""
348
+
349
+ try:
350
+ resp = client.chat.completions.create(
351
+ model=model,
352
+ messages=[{"role": "user", "content": prompt}],
353
+ response_format={"type": "json_object"},
354
+ max_tokens=256,
355
+ temperature=0.2,
356
+ )
357
+ content = resp.choices[0].message.content
358
+ content = re.sub(r'<think>[\s\S]*?</think>', '', content).strip()
359
+ filters = json.loads(content)
360
+ # Clean empty values
361
+ return {k: v for k, v in filters.items() if v is not None and v != "" and v != []}
362
+ except Exception:
363
+ return {}
364
+
365
+
366
  @app.post("/api/cohort/generate")
367
  async def generate_cohort_endpoint(config: CohortConfig):
368
  """Generate a cohort — from Nemotron if available, else LLM-generated."""
 
375
  import random
376
  pl = _lazy_persona_loader()
377
  ss = _lazy_stratified_sampler()
378
+
379
+ # Extract structured filters from audience context
380
+ client = get_client()
381
+ model = get_model()
382
+ filters = extract_filters(client, model, config.audience_context, config.description)
383
+ print(f"Nemotron filters from audience context: {filters}")
384
+
385
+ filtered = pl.filter_personas(ds, filters, limit=max(total * 20, 2000))
386
  profiles = [pl.to_profile(row, i) for i, row in enumerate(filtered)]
387
 
388
  # Use only age + education to keep strata count < total
 
431
  return {
432
  "session_id": sid, "cohort_size": len(all_personas),
433
  "cohort": all_personas, "source": source,
434
+ "filters": filters if ds is not None else None,
435
  }
436
 
437
 
web/static/index.html CHANGED
@@ -719,7 +719,7 @@ async function runFullPipeline() {
719
  const cohortResp = await fetch('/api/cohort/generate', {
720
  method: 'POST',
721
  headers: {'Content-Type': 'application/json'},
722
- body: JSON.stringify({description: desc, segments, parallel: 3}),
723
  });
724
  const cohortData = await cohortResp.json();
725
 
@@ -731,6 +731,16 @@ async function runFullPipeline() {
731
  });
732
 
733
  const src = cohortData.source === 'nemotron' ? 'census-grounded (Nemotron)' : 'LLM-generated';
 
 
 
 
 
 
 
 
 
 
734
  logStep(`${cohortData.cohort_size} evaluators ready — ${src}`, 'pos');
735
  if (panelSize < 40) {
736
  logStep(`Tip: a panel of 40-60 gives more reliable results across segments`, 'neu');
 
719
  const cohortResp = await fetch('/api/cohort/generate', {
720
  method: 'POST',
721
  headers: {'Content-Type': 'application/json'},
722
+ body: JSON.stringify({description: desc, audience_context: audienceCtx, segments, parallel: 3}),
723
  });
724
  const cohortData = await cohortResp.json();
725
 
 
731
  });
732
 
733
  const src = cohortData.source === 'nemotron' ? 'census-grounded (Nemotron)' : 'LLM-generated';
734
+ if (cohortData.filters && Object.keys(cohortData.filters).length > 0) {
735
+ const f = cohortData.filters;
736
+ const parts = [];
737
+ if (f.sex) parts.push(f.sex);
738
+ if (f.age_min || f.age_max) parts.push(`age ${f.age_min||'?'}-${f.age_max||'?'}`);
739
+ if (f.city) parts.push(f.city);
740
+ if (f.state) parts.push(f.state);
741
+ if (f.occupation) parts.push(f.occupation);
742
+ logStep(`Filtered: ${parts.join(', ')}`, 'pos');
743
+ }
744
  logStep(`${cohortData.cohort_size} evaluators ready — ${src}`, 'pos');
745
  if (panelSize < 40) {
746
  logStep(`Tip: a panel of 40-60 gives more reliable results across segments`, 'neu');