Spaces:
Running
Running
Tune optimizer acceptance policy and add optimization modes
Browse filesImprove edit acceptance by supporting conservative/balanced/aggressive modes, continuing after rejected steps, prioritizing goal-level gains, and surfacing rejection reasons in iteration logs.
Made-with: Cursor
- models.py +2 -0
- optimizer.py +71 -15
- templates/index.html +19 -4
models.py
CHANGED
|
@@ -90,6 +90,7 @@ class OptimizerRequest(BaseModel):
|
|
| 90 |
max_iterations: int = 2
|
| 91 |
candidates_per_iteration: int = 2
|
| 92 |
temperature: float = 0.25
|
|
|
|
| 93 |
|
| 94 |
|
| 95 |
class OptimizerResponse(BaseModel):
|
|
@@ -99,4 +100,5 @@ class OptimizerResponse(BaseModel):
|
|
| 99 |
final_metrics: Dict[str, Any] = Field(default_factory=dict)
|
| 100 |
iterations: List[Dict[str, Any]] = Field(default_factory=list)
|
| 101 |
applied_changes: int = 0
|
|
|
|
| 102 |
error: str = ""
|
|
|
|
| 90 |
max_iterations: int = 2
|
| 91 |
candidates_per_iteration: int = 2
|
| 92 |
temperature: float = 0.25
|
| 93 |
+
optimization_mode: str = "balanced"
|
| 94 |
|
| 95 |
|
| 96 |
class OptimizerResponse(BaseModel):
|
|
|
|
| 100 |
final_metrics: Dict[str, Any] = Field(default_factory=dict)
|
| 101 |
iterations: List[Dict[str, Any]] = Field(default_factory=list)
|
| 102 |
applied_changes: int = 0
|
| 103 |
+
optimization_mode: str = "balanced"
|
| 104 |
error: str = ""
|
optimizer.py
CHANGED
|
@@ -358,18 +358,59 @@ def _llm_rewrite_sentence(
|
|
| 358 |
return str(parsed["revised_sentence"]).strip()
|
| 359 |
|
| 360 |
|
| 361 |
-
def
|
| 362 |
-
if
|
| 363 |
-
return
|
| 364 |
-
if
|
| 365 |
-
return
|
| 366 |
-
if
|
| 367 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
prev_title = prev_metrics.get("title_bert_score")
|
| 369 |
next_title = next_metrics.get("title_bert_score")
|
| 370 |
-
if prev_title is not None and next_title is not None and next_title < (prev_title -
|
| 371 |
-
|
| 372 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
|
| 374 |
|
| 375 |
def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -390,6 +431,7 @@ def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 390 |
candidates_per_iteration = int(request_data.get("candidates_per_iteration", 2) or 2)
|
| 391 |
candidates_per_iteration = max(1, min(5, candidates_per_iteration))
|
| 392 |
temperature = float(request_data.get("temperature", 0.25) or 0.25)
|
|
|
|
| 393 |
|
| 394 |
baseline_analysis = _build_analysis_snapshot(
|
| 395 |
target_text, competitors, keywords, language, target_title, competitor_titles
|
|
@@ -447,7 +489,9 @@ def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 447 |
)
|
| 448 |
cand_semantic = _build_semantic_snapshot(candidate_text, competitors, language)
|
| 449 |
cand_metrics = _compute_metrics(cand_analysis, cand_semantic, keywords, language)
|
| 450 |
-
valid = _is_candidate_valid(
|
|
|
|
|
|
|
| 451 |
delta_score = round(cand_metrics["score"] - current_metrics["score"], 3)
|
| 452 |
candidates.append(
|
| 453 |
{
|
|
@@ -458,6 +502,8 @@ def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 458 |
"semantic": cand_semantic,
|
| 459 |
"metrics": cand_metrics,
|
| 460 |
"valid": valid,
|
|
|
|
|
|
|
| 461 |
"delta_score": delta_score,
|
| 462 |
}
|
| 463 |
)
|
|
@@ -467,6 +513,8 @@ def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 467 |
"candidate_index": ci + 1,
|
| 468 |
"error": str(e),
|
| 469 |
"valid": False,
|
|
|
|
|
|
|
| 470 |
"delta_score": -999.0,
|
| 471 |
}
|
| 472 |
)
|
|
@@ -484,6 +532,8 @@ def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 484 |
{
|
| 485 |
"candidate_index": c.get("candidate_index"),
|
| 486 |
"valid": c.get("valid", False),
|
|
|
|
|
|
|
| 487 |
"delta_score": c.get("delta_score"),
|
| 488 |
"error": c.get("error"),
|
| 489 |
}
|
|
@@ -491,10 +541,15 @@ def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 491 |
],
|
| 492 |
}
|
| 493 |
)
|
| 494 |
-
|
| 495 |
|
| 496 |
-
best = sorted(
|
| 497 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
logs.append(
|
| 499 |
{
|
| 500 |
"step": step + 1,
|
|
@@ -506,7 +561,7 @@ def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 506 |
"current_score": current_metrics["score"],
|
| 507 |
}
|
| 508 |
)
|
| 509 |
-
|
| 510 |
|
| 511 |
prev_metrics = current_metrics
|
| 512 |
current_text = best["text"]
|
|
@@ -535,4 +590,5 @@ def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 535 |
"final_metrics": current_metrics,
|
| 536 |
"iterations": logs,
|
| 537 |
"applied_changes": applied_changes,
|
|
|
|
| 538 |
}
|
|
|
|
| 358 |
return str(parsed["revised_sentence"]).strip()
|
| 359 |
|
| 360 |
|
| 361 |
+
def _goal_improved(goal_type: str, prev_metrics: Dict[str, Any], next_metrics: Dict[str, Any]) -> bool:
|
| 362 |
+
if goal_type == "bert":
|
| 363 |
+
return next_metrics["bert_low_count"] < prev_metrics["bert_low_count"]
|
| 364 |
+
if goal_type == "bm25":
|
| 365 |
+
return next_metrics["bm25_remove_count"] < prev_metrics["bm25_remove_count"]
|
| 366 |
+
if goal_type == "semantic":
|
| 367 |
+
return next_metrics["semantic_gap_count"] < prev_metrics["semantic_gap_count"]
|
| 368 |
+
if goal_type == "ngram":
|
| 369 |
+
return next_metrics["ngram_signal_count"] < prev_metrics["ngram_signal_count"]
|
| 370 |
+
return next_metrics["score"] > prev_metrics["score"]
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def _is_candidate_valid(
|
| 374 |
+
prev_metrics: Dict[str, Any],
|
| 375 |
+
next_metrics: Dict[str, Any],
|
| 376 |
+
goal_type: str,
|
| 377 |
+
optimization_mode: str,
|
| 378 |
+
) -> Tuple[bool, List[str], bool]:
|
| 379 |
+
mode = (optimization_mode or "balanced").lower()
|
| 380 |
+
if mode not in {"conservative", "balanced", "aggressive"}:
|
| 381 |
+
mode = "balanced"
|
| 382 |
+
|
| 383 |
+
cfg = {
|
| 384 |
+
"conservative": {"max_score_drop": 0.0, "max_title_drop": 0.02},
|
| 385 |
+
"balanced": {"max_score_drop": 1.0, "max_title_drop": 0.03},
|
| 386 |
+
"aggressive": {"max_score_drop": 2.0, "max_title_drop": 0.05},
|
| 387 |
+
}[mode]
|
| 388 |
+
|
| 389 |
+
reasons = []
|
| 390 |
+
score_drop = float(prev_metrics["score"]) - float(next_metrics["score"])
|
| 391 |
+
if score_drop > cfg["max_score_drop"]:
|
| 392 |
+
reasons.append(f"score_drop>{cfg['max_score_drop']}")
|
| 393 |
+
|
| 394 |
+
# Hard regressions in critical counters.
|
| 395 |
+
if next_metrics["bm25_remove_count"] > prev_metrics["bm25_remove_count"] + (1 if mode == "aggressive" else 0):
|
| 396 |
+
reasons.append("bm25_remove_regression")
|
| 397 |
+
if next_metrics["bert_low_count"] > prev_metrics["bert_low_count"] + (1 if mode == "aggressive" else 0):
|
| 398 |
+
reasons.append("bert_low_regression")
|
| 399 |
+
if next_metrics["semantic_gap_count"] > prev_metrics["semantic_gap_count"] + (1 if mode == "aggressive" else 0):
|
| 400 |
+
reasons.append("semantic_gap_regression")
|
| 401 |
+
|
| 402 |
prev_title = prev_metrics.get("title_bert_score")
|
| 403 |
next_title = next_metrics.get("title_bert_score")
|
| 404 |
+
if prev_title is not None and next_title is not None and next_title < (prev_title - cfg["max_title_drop"]):
|
| 405 |
+
reasons.append("title_bert_drop")
|
| 406 |
+
|
| 407 |
+
improved = _goal_improved(goal_type, prev_metrics, next_metrics)
|
| 408 |
+
|
| 409 |
+
# In conservative mode require explicit goal improvement.
|
| 410 |
+
if mode == "conservative" and not improved:
|
| 411 |
+
reasons.append("goal_not_improved")
|
| 412 |
+
|
| 413 |
+
return (len(reasons) == 0), reasons, improved
|
| 414 |
|
| 415 |
|
| 416 |
def optimize_text(request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
| 431 |
candidates_per_iteration = int(request_data.get("candidates_per_iteration", 2) or 2)
|
| 432 |
candidates_per_iteration = max(1, min(5, candidates_per_iteration))
|
| 433 |
temperature = float(request_data.get("temperature", 0.25) or 0.25)
|
| 434 |
+
optimization_mode = str(request_data.get("optimization_mode", "balanced") or "balanced")
|
| 435 |
|
| 436 |
baseline_analysis = _build_analysis_snapshot(
|
| 437 |
target_text, competitors, keywords, language, target_title, competitor_titles
|
|
|
|
| 489 |
)
|
| 490 |
cand_semantic = _build_semantic_snapshot(candidate_text, competitors, language)
|
| 491 |
cand_metrics = _compute_metrics(cand_analysis, cand_semantic, keywords, language)
|
| 492 |
+
valid, invalid_reasons, goal_improved = _is_candidate_valid(
|
| 493 |
+
current_metrics, cand_metrics, goal["type"], optimization_mode
|
| 494 |
+
)
|
| 495 |
delta_score = round(cand_metrics["score"] - current_metrics["score"], 3)
|
| 496 |
candidates.append(
|
| 497 |
{
|
|
|
|
| 502 |
"semantic": cand_semantic,
|
| 503 |
"metrics": cand_metrics,
|
| 504 |
"valid": valid,
|
| 505 |
+
"goal_improved": goal_improved,
|
| 506 |
+
"invalid_reasons": invalid_reasons,
|
| 507 |
"delta_score": delta_score,
|
| 508 |
}
|
| 509 |
)
|
|
|
|
| 513 |
"candidate_index": ci + 1,
|
| 514 |
"error": str(e),
|
| 515 |
"valid": False,
|
| 516 |
+
"goal_improved": False,
|
| 517 |
+
"invalid_reasons": [str(e)],
|
| 518 |
"delta_score": -999.0,
|
| 519 |
}
|
| 520 |
)
|
|
|
|
| 532 |
{
|
| 533 |
"candidate_index": c.get("candidate_index"),
|
| 534 |
"valid": c.get("valid", False),
|
| 535 |
+
"goal_improved": c.get("goal_improved", False),
|
| 536 |
+
"invalid_reasons": c.get("invalid_reasons", []),
|
| 537 |
"delta_score": c.get("delta_score"),
|
| 538 |
"error": c.get("error"),
|
| 539 |
}
|
|
|
|
| 541 |
],
|
| 542 |
}
|
| 543 |
)
|
| 544 |
+
continue
|
| 545 |
|
| 546 |
+
best = sorted(
|
| 547 |
+
valid_candidates,
|
| 548 |
+
key=lambda c: (1 if c.get("goal_improved") else 0, c["metrics"]["score"]),
|
| 549 |
+
reverse=True,
|
| 550 |
+
)[0]
|
| 551 |
+
# Accept candidate if it improves goal OR improves total score.
|
| 552 |
+
if not best.get("goal_improved") and best["metrics"]["score"] <= current_metrics["score"]:
|
| 553 |
logs.append(
|
| 554 |
{
|
| 555 |
"step": step + 1,
|
|
|
|
| 561 |
"current_score": current_metrics["score"],
|
| 562 |
}
|
| 563 |
)
|
| 564 |
+
continue
|
| 565 |
|
| 566 |
prev_metrics = current_metrics
|
| 567 |
current_text = best["text"]
|
|
|
|
| 590 |
"final_metrics": current_metrics,
|
| 591 |
"iterations": logs,
|
| 592 |
"applied_changes": applied_changes,
|
| 593 |
+
"optimization_mode": optimization_mode,
|
| 594 |
}
|
templates/index.html
CHANGED
|
@@ -302,6 +302,14 @@
|
|
| 302 |
<label class="form-label small text-muted mb-1">Temp</label>
|
| 303 |
<input type="number" id="optimizerTemp" class="form-control" min="0" max="1.2" step="0.05" value="0.25">
|
| 304 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
</div>
|
| 306 |
<div class="d-flex gap-2 mt-3">
|
| 307 |
<button class="btn btn-dark" onclick="runLlmOptimization()">Запустить оптимизацию</button>
|
|
@@ -536,7 +544,8 @@
|
|
| 536 |
optimizer_model: document.getElementById('optimizerModel').value,
|
| 537 |
optimizer_iterations: Number(document.getElementById('optimizerIterations').value || 2),
|
| 538 |
optimizer_candidates: Number(document.getElementById('optimizerCandidates').value || 2),
|
| 539 |
-
optimizer_temperature: Number(document.getElementById('optimizerTemp').value || 0.25)
|
|
|
|
| 540 |
},
|
| 541 |
state: {
|
| 542 |
analysis_result: currentData,
|
|
@@ -586,6 +595,7 @@
|
|
| 586 |
document.getElementById('optimizerIterations').value = 2;
|
| 587 |
document.getElementById('optimizerCandidates').value = 2;
|
| 588 |
document.getElementById('optimizerTemp').value = 0.25;
|
|
|
|
| 589 |
|
| 590 |
// Competitor text fields
|
| 591 |
const competitorsList = document.getElementById('competitorsList');
|
|
@@ -638,6 +648,7 @@
|
|
| 638 |
document.getElementById('optimizerIterations').value = inp.optimizer_iterations ?? 2;
|
| 639 |
document.getElementById('optimizerCandidates').value = inp.optimizer_candidates ?? 2;
|
| 640 |
document.getElementById('optimizerTemp').value = inp.optimizer_temperature ?? 0.25;
|
|
|
|
| 641 |
|
| 642 |
// Title character counter refresh
|
| 643 |
const titleLen = (inp.target_title || '').length;
|
|
@@ -817,6 +828,7 @@
|
|
| 817 |
const iterRows = (data.iterations || []).map(it => {
|
| 818 |
const before = it.metrics_before ? it.metrics_before.score : '-';
|
| 819 |
const after = it.metrics_after ? it.metrics_after.score : '-';
|
|
|
|
| 820 |
return `<tr>
|
| 821 |
<td>${it.step}</td>
|
| 822 |
<td>${it.status}</td>
|
|
@@ -824,6 +836,7 @@
|
|
| 824 |
<td>${before}</td>
|
| 825 |
<td>${after}</td>
|
| 826 |
<td>${it.delta_score ?? '-'}</td>
|
|
|
|
| 827 |
</tr>`;
|
| 828 |
}).join('');
|
| 829 |
|
|
@@ -831,6 +844,7 @@
|
|
| 831 |
<div class="stat-card">
|
| 832 |
<h6 class="card-title">Результат оптимизации</h6>
|
| 833 |
<div class="small mb-2">Применено правок: <strong>${data.applied_changes || 0}</strong></div>
|
|
|
|
| 834 |
<div class="table-responsive">
|
| 835 |
<table class="table table-sm table-bordered mb-0">
|
| 836 |
<thead class="table-light"><tr><th>Метрика</th><th>До</th><th>После</th></tr></thead>
|
|
@@ -842,8 +856,8 @@
|
|
| 842 |
<h6 class="card-title">Лог итераций</h6>
|
| 843 |
<div class="table-responsive">
|
| 844 |
<table class="table table-sm table-hover mb-0">
|
| 845 |
-
<thead><tr><th>#</th><th>Статус</th><th>Цель</th><th>Score до</th><th>Score после</th><th>Δ</th></tr></thead>
|
| 846 |
-
<tbody>${iterRows || '<tr><td colspan="
|
| 847 |
</table>
|
| 848 |
</div>
|
| 849 |
</div>`;
|
|
@@ -882,7 +896,8 @@
|
|
| 882 |
model: (document.getElementById('optimizerModel').value || '').trim(),
|
| 883 |
max_iterations: Number(document.getElementById('optimizerIterations').value || 2),
|
| 884 |
candidates_per_iteration: Number(document.getElementById('optimizerCandidates').value || 2),
|
| 885 |
-
temperature: Number(document.getElementById('optimizerTemp').value || 0.25)
|
|
|
|
| 886 |
};
|
| 887 |
|
| 888 |
document.getElementById('loader').style.display = 'block';
|
|
|
|
| 302 |
<label class="form-label small text-muted mb-1">Temp</label>
|
| 303 |
<input type="number" id="optimizerTemp" class="form-control" min="0" max="1.2" step="0.05" value="0.25">
|
| 304 |
</div>
|
| 305 |
+
<div class="col-md-3">
|
| 306 |
+
<label class="form-label small text-muted mb-1">Режим оптимизации</label>
|
| 307 |
+
<select id="optimizerMode" class="form-select">
|
| 308 |
+
<option value="conservative">Conservative</option>
|
| 309 |
+
<option value="balanced" selected>Balanced</option>
|
| 310 |
+
<option value="aggressive">Aggressive</option>
|
| 311 |
+
</select>
|
| 312 |
+
</div>
|
| 313 |
</div>
|
| 314 |
<div class="d-flex gap-2 mt-3">
|
| 315 |
<button class="btn btn-dark" onclick="runLlmOptimization()">Запустить оптимизацию</button>
|
|
|
|
| 544 |
optimizer_model: document.getElementById('optimizerModel').value,
|
| 545 |
optimizer_iterations: Number(document.getElementById('optimizerIterations').value || 2),
|
| 546 |
optimizer_candidates: Number(document.getElementById('optimizerCandidates').value || 2),
|
| 547 |
+
optimizer_temperature: Number(document.getElementById('optimizerTemp').value || 0.25),
|
| 548 |
+
optimizer_mode: document.getElementById('optimizerMode').value
|
| 549 |
},
|
| 550 |
state: {
|
| 551 |
analysis_result: currentData,
|
|
|
|
| 595 |
document.getElementById('optimizerIterations').value = 2;
|
| 596 |
document.getElementById('optimizerCandidates').value = 2;
|
| 597 |
document.getElementById('optimizerTemp').value = 0.25;
|
| 598 |
+
document.getElementById('optimizerMode').value = 'balanced';
|
| 599 |
|
| 600 |
// Competitor text fields
|
| 601 |
const competitorsList = document.getElementById('competitorsList');
|
|
|
|
| 648 |
document.getElementById('optimizerIterations').value = inp.optimizer_iterations ?? 2;
|
| 649 |
document.getElementById('optimizerCandidates').value = inp.optimizer_candidates ?? 2;
|
| 650 |
document.getElementById('optimizerTemp').value = inp.optimizer_temperature ?? 0.25;
|
| 651 |
+
document.getElementById('optimizerMode').value = inp.optimizer_mode || 'balanced';
|
| 652 |
|
| 653 |
// Title character counter refresh
|
| 654 |
const titleLen = (inp.target_title || '').length;
|
|
|
|
| 828 |
const iterRows = (data.iterations || []).map(it => {
|
| 829 |
const before = it.metrics_before ? it.metrics_before.score : '-';
|
| 830 |
const after = it.metrics_after ? it.metrics_after.score : '-';
|
| 831 |
+
const reason = it.reason || (it.candidates ? 'all candidates rejected by constraints' : '-');
|
| 832 |
return `<tr>
|
| 833 |
<td>${it.step}</td>
|
| 834 |
<td>${it.status}</td>
|
|
|
|
| 836 |
<td>${before}</td>
|
| 837 |
<td>${after}</td>
|
| 838 |
<td>${it.delta_score ?? '-'}</td>
|
| 839 |
+
<td>${reason}</td>
|
| 840 |
</tr>`;
|
| 841 |
}).join('');
|
| 842 |
|
|
|
|
| 844 |
<div class="stat-card">
|
| 845 |
<h6 class="card-title">Результат оптимизации</h6>
|
| 846 |
<div class="small mb-2">Применено правок: <strong>${data.applied_changes || 0}</strong></div>
|
| 847 |
+
<div class="small mb-2">Режим: <strong>${data.optimization_mode || 'balanced'}</strong></div>
|
| 848 |
<div class="table-responsive">
|
| 849 |
<table class="table table-sm table-bordered mb-0">
|
| 850 |
<thead class="table-light"><tr><th>Метрика</th><th>До</th><th>После</th></tr></thead>
|
|
|
|
| 856 |
<h6 class="card-title">Лог итераций</h6>
|
| 857 |
<div class="table-responsive">
|
| 858 |
<table class="table table-sm table-hover mb-0">
|
| 859 |
+
<thead><tr><th>#</th><th>Статус</th><th>Цель</th><th>Score до</th><th>Score после</th><th>Δ</th><th>Причина/комментарий</th></tr></thead>
|
| 860 |
+
<tbody>${iterRows || '<tr><td colspan="7" class="text-muted text-center">Нет данных</td></tr>'}</tbody>
|
| 861 |
</table>
|
| 862 |
</div>
|
| 863 |
</div>`;
|
|
|
|
| 896 |
model: (document.getElementById('optimizerModel').value || '').trim(),
|
| 897 |
max_iterations: Number(document.getElementById('optimizerIterations').value || 2),
|
| 898 |
candidates_per_iteration: Number(document.getElementById('optimizerCandidates').value || 2),
|
| 899 |
+
temperature: Number(document.getElementById('optimizerTemp').value || 0.25),
|
| 900 |
+
optimization_mode: document.getElementById('optimizerMode').value || 'balanced'
|
| 901 |
};
|
| 902 |
|
| 903 |
document.getElementById('loader').style.display = 'block';
|