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

Make entity, goal, and audience first-class inputs

Browse files

Three equal inputs define the SGO spec:
- Entity: what you're putting out there
- Goal: what outcome you want
- Audience: who evaluates it

Goal and audience are auto-inferred from entity via LLM if left blank,
but visible and editable so the user always sees and controls the spec.
Audience context now drives Nemotron demographic filtering directly.

- Add /api/infer-spec endpoint
- Promote audience from hidden Advanced Options to main form
- Auto-fill on Evaluate if blank, show in progress log

Files changed (2) hide show
  1. web/app.py +46 -0
  2. web/static/index.html +45 -8
web/app.py CHANGED
@@ -235,6 +235,52 @@ async def get_session(sid: str):
235
  }
236
 
237
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  @app.post("/api/suggest-changes")
239
  async def suggest_changes(input: SuggestChangesInput):
240
  """Generate candidate changes from evaluation concerns and goal."""
 
235
  }
236
 
237
 
238
+ class InferSpecInput(BaseModel):
239
+ entity_text: str
240
+
241
+
242
+ @app.post("/api/infer-spec")
243
+ async def infer_spec(input: InferSpecInput):
244
+ """Infer goal and audience from entity text."""
245
+ client = get_client()
246
+ model = get_model()
247
+
248
+ prompt = f"""Read this entity and infer two things:
249
+ 1. What is the most likely GOAL the author has? (what outcome they want)
250
+ 2. Who is the intended AUDIENCE? (who evaluates or decides)
251
+
252
+ Entity:
253
+ {input.entity_text[:2000]}
254
+
255
+ Return JSON:
256
+ {{
257
+ "goal": "<1 sentence — the outcome they're optimizing for>",
258
+ "audience": "<1 sentence — who should evaluate this, with demographics if obvious>"
259
+ }}
260
+
261
+ Examples:
262
+ - Product landing page → goal: "Convert visitors to paying customers", audience: "Software developers evaluating dev tools"
263
+ - Resume → goal: "Get interview callbacks from target companies", audience: "Engineering hiring managers at mid-stage startups"
264
+ - Profile → goal: "Attract compatible connections", audience: "Professionals aged 28-40 in the same metro area"
265
+ - Pitch deck → goal: "Secure Series A funding", audience: "VCs and angels focused on B2B SaaS"
266
+
267
+ Be specific to THIS entity, not generic."""
268
+
269
+ try:
270
+ resp = client.chat.completions.create(
271
+ model=model,
272
+ messages=[{"role": "user", "content": prompt}],
273
+ response_format={"type": "json_object"},
274
+ max_tokens=256,
275
+ temperature=0.5,
276
+ )
277
+ content = resp.choices[0].message.content
278
+ content = re.sub(r'<think>[\s\S]*?</think>', '', content).strip()
279
+ return json.loads(content)
280
+ except Exception as e:
281
+ raise HTTPException(500, f"Failed to infer spec: {e}")
282
+
283
+
284
  @app.post("/api/suggest-changes")
285
  async def suggest_changes(input: SuggestChangesInput):
286
  """Generate candidate changes from evaluation concerns and goal."""
web/static/index.html CHANGED
@@ -345,21 +345,23 @@
345
  </div>
346
 
347
  <div class="field">
348
- <textarea id="entityText" placeholder="Paste your entity here..."></textarea>
 
349
  </div>
350
 
351
  <div class="field">
352
- <label>What's your goal?</label>
353
- <input type="text" id="goalText" placeholder="e.g. 'Get hired at a Series B startup' or 'Close enterprise deals'">
 
 
 
 
 
354
  </div>
355
 
356
  <details class="mb-8">
357
  <summary style="cursor:pointer;color:var(--text2);font-size:0.85rem">Advanced options</summary>
358
  <div style="padding:12px 0">
359
- <div class="field">
360
- <label>Audience context (optional — auto-detected if blank)</label>
361
- <input type="text" id="cohortDesc" placeholder="e.g. 'Would customers buy this product?'">
362
- </div>
363
  <div class="field">
364
  <label>Panel size</label>
365
  <input type="number" id="panelSize" value="30" min="5" max="80"
@@ -656,6 +658,30 @@ function logStep(msg, cls = '') {
656
 
657
  // ── Step 1: Full pipeline (one click) ──
658
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659
  async function runFullPipeline() {
660
  const text = document.getElementById('entityText').value.trim();
661
  if (!text) return alert('Please enter your entity text.');
@@ -669,9 +695,20 @@ async function runFullPipeline() {
669
  document.getElementById('evalLog').innerHTML = '';
670
  document.getElementById('pipelineProgressBar').style.width = '5%';
671
 
 
 
 
 
 
 
 
 
 
 
 
672
  const biasCal = document.getElementById('biasCalibration').checked;
673
  const panelSize = parseInt(document.getElementById('panelSize').value) || 30;
674
- const audienceCtx = document.getElementById('cohortDesc').value.trim();
675
 
676
  try {
677
  // Phase 1: Create session
 
345
  </div>
346
 
347
  <div class="field">
348
+ <label>Entity what are you putting out there?</label>
349
+ <textarea id="entityText" placeholder="Paste your landing page, resume, pitch, profile, policy..."></textarea>
350
  </div>
351
 
352
  <div class="field">
353
+ <label>Goal what outcome do you want?</label>
354
+ <input type="text" id="goalText" placeholder="Will be auto-suggested from your entity">
355
+ </div>
356
+
357
+ <div class="field">
358
+ <label>Audience — who are you trying to reach?</label>
359
+ <input type="text" id="cohortDesc" placeholder="Will be auto-suggested from your entity">
360
  </div>
361
 
362
  <details class="mb-8">
363
  <summary style="cursor:pointer;color:var(--text2);font-size:0.85rem">Advanced options</summary>
364
  <div style="padding:12px 0">
 
 
 
 
365
  <div class="field">
366
  <label>Panel size</label>
367
  <input type="number" id="panelSize" value="30" min="5" max="80"
 
658
 
659
  // ── Step 1: Full pipeline (one click) ──
660
 
661
+ async function inferSpec() {
662
+ const text = document.getElementById('entityText').value.trim();
663
+ if (!text) return;
664
+
665
+ const goalField = document.getElementById('goalText');
666
+ const audienceField = document.getElementById('cohortDesc');
667
+
668
+ // Only auto-fill if both are empty
669
+ if (goalField.value.trim() && audienceField.value.trim()) return;
670
+
671
+ try {
672
+ const resp = await fetch('/api/infer-spec', {
673
+ method: 'POST',
674
+ headers: {'Content-Type': 'application/json'},
675
+ body: JSON.stringify({entity_text: text}),
676
+ });
677
+ const data = await resp.json();
678
+ if (!goalField.value.trim() && data.goal) goalField.value = data.goal;
679
+ if (!audienceField.value.trim() && data.audience) audienceField.value = data.audience;
680
+ } catch (e) {
681
+ // Silent fail — user can fill in manually
682
+ }
683
+ }
684
+
685
  async function runFullPipeline() {
686
  const text = document.getElementById('entityText').value.trim();
687
  if (!text) return alert('Please enter your entity text.');
 
695
  document.getElementById('evalLog').innerHTML = '';
696
  document.getElementById('pipelineProgressBar').style.width = '5%';
697
 
698
+ // Auto-infer goal + audience if not provided
699
+ const goalField = document.getElementById('goalText');
700
+ const audienceField = document.getElementById('cohortDesc');
701
+ if (!goalField.value.trim() || !audienceField.value.trim()) {
702
+ document.getElementById('pipelineProgressText').textContent = 'Inferring goal and audience from entity...';
703
+ logStep('Inferring goal and audience from entity...');
704
+ await inferSpec();
705
+ if (goalField.value) logStep(`Goal: ${goalField.value}`, 'pos');
706
+ if (audienceField.value) logStep(`Audience: ${audienceField.value}`, 'pos');
707
+ }
708
+
709
  const biasCal = document.getElementById('biasCalibration').checked;
710
  const panelSize = parseInt(document.getElementById('panelSize').value) || 30;
711
+ const audienceCtx = audienceField.value.trim();
712
 
713
  try {
714
  // Phase 1: Create session