lsdf commited on
Commit
eb8813a
·
1 Parent(s): dc860ce

Tune optimizer acceptance policy and add optimization modes

Browse files

Improve 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

Files changed (3) hide show
  1. models.py +2 -0
  2. optimizer.py +71 -15
  3. 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 _is_candidate_valid(prev_metrics: Dict[str, Any], next_metrics: Dict[str, Any]) -> bool:
362
- if next_metrics["bert_low_count"] > prev_metrics["bert_low_count"]:
363
- return False
364
- if next_metrics["bm25_remove_count"] > prev_metrics["bm25_remove_count"]:
365
- return False
366
- if next_metrics["semantic_gap_count"] > prev_metrics["semantic_gap_count"]:
367
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 - 0.03):
371
- return False
372
- return True
 
 
 
 
 
 
 
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(current_metrics, cand_metrics)
 
 
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
- break
495
 
496
- best = sorted(valid_candidates, key=lambda c: c["metrics"]["score"], reverse=True)[0]
497
- if best["metrics"]["score"] <= current_metrics["score"]:
 
 
 
 
 
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
- break
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="6" class="text-muted text-center">Нет данных</td></tr>'}</tbody>
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';