Eric Xu commited on
Make entity, goal, and audience first-class inputs
Browse filesThree 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
- web/app.py +46 -0
- 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 |
-
<
|
|
|
|
| 349 |
</div>
|
| 350 |
|
| 351 |
<div class="field">
|
| 352 |
-
<label>
|
| 353 |
-
<input type="text" id="goalText" placeholder="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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
|