feat(zopk): Re-ewaluacja newsów z niską oceną zawierających kluczowe tematy
- Nowa funkcja reevaluate_low_score_news() szuka newsów z 1-2★
zawierających Via Pomerania, NORDA, S6, Droga Czerwona, etc.
- Nowy endpoint POST /admin/zopk/news/reevaluate-low-scores
- Przycisk w UI "Re-ewaluuj niskie oceny" z szczegółowym raportem
- Automatyczne auto-approve jeśli nowa ocena >= 3★
Problem: Artykuły o Via Pomerania miały 1★ bo były ocenione
przed dodaniem tego tematu do promptu AI.
Rozwiązanie: Re-ewaluacja nowym promptem podniesie ich oceny.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0cbdcaaad6
commit
1193a2bf48
43
app.py
43
app.py
@ -10673,6 +10673,49 @@ def admin_zopk_reevaluate_scores():
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/admin/zopk/news/reevaluate-low-scores', methods=['POST'])
|
||||
@login_required
|
||||
def admin_zopk_reevaluate_low_scores():
|
||||
"""
|
||||
Re-evaluate news with low AI scores (1-2★) that contain key ZOPK topics.
|
||||
|
||||
Useful after updating AI prompt to include new topics like Via Pomerania, S6, NORDA.
|
||||
Old articles scored low before these topics were recognized will be re-evaluated
|
||||
and potentially upgraded.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_news_service import reevaluate_low_score_news
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
limit = data.get('limit', 50) # Max 50 to avoid API limits
|
||||
|
||||
# Run AI re-evaluation for low-score items with key topics
|
||||
result = reevaluate_low_score_news(db, limit=limit, user_id=current_user.id)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'total_evaluated': result.get('total_evaluated', 0),
|
||||
'upgraded': result.get('upgraded', 0),
|
||||
'downgraded': result.get('downgraded', 0),
|
||||
'unchanged': result.get('unchanged', 0),
|
||||
'errors': result.get('errors', 0),
|
||||
'message': result.get('message', ''),
|
||||
'details': result.get('details', [])
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error reevaluating low-score ZOPK news: {e}")
|
||||
return jsonify({'success': False, 'error': 'Wystąpił błąd podczas ponownej oceny'}), 500
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/api/zopk/search-news', methods=['POST'])
|
||||
@login_required
|
||||
def api_zopk_search_news():
|
||||
|
||||
@ -1072,6 +1072,14 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Re-evaluate low score news with key topics (Via Pomerania, NORDA, etc.) -->
|
||||
<div class="stats-grid" style="grid-template-columns: repeat(1, 1fr); max-width: 400px; margin-top: var(--spacing-md);">
|
||||
<button type="button" class="stat-card filter-card ai-action" onclick="reevaluateLowScores()" id="aiReevalLowBtn" title="Re-ewaluacja newsów z oceną 1-2★ zawierających nowe tematy (Via Pomerania, NORDA, S6...)">
|
||||
<div class="stat-value" style="font-size: var(--font-size-xl);">🔄⭐</div>
|
||||
<div class="stat-label">Re-ewaluuj niskie oceny (Via Pomerania, NORDA...)</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="aiEvalResult" style="margin-top: var(--spacing-md); display: none;"></div>
|
||||
</div>
|
||||
|
||||
@ -1924,6 +1932,126 @@ async function reevaluateScores() {
|
||||
}
|
||||
}
|
||||
|
||||
// Re-evaluate low score news (1-2★) with key topics (Via Pomerania, NORDA, etc.)
|
||||
async function reevaluateLowScores() {
|
||||
const btn = document.getElementById('aiReevalLowBtn');
|
||||
if (!btn) return;
|
||||
|
||||
// Show modal for re-evaluation
|
||||
document.getElementById('aiEvalModal').classList.add('active');
|
||||
document.getElementById('aiEvalConfirm').style.display = 'none';
|
||||
document.getElementById('aiEvalProgress').style.display = 'block';
|
||||
document.getElementById('aiEvalResult').style.display = 'none';
|
||||
|
||||
const progressBar = document.getElementById('aiProgressFill');
|
||||
const progressStatus = document.getElementById('aiProgressStatus');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.querySelector('.stat-label').textContent = 'Re-ewaluacja...';
|
||||
|
||||
// Simulated progress for better UX
|
||||
let progress = 0;
|
||||
let startTime = Date.now();
|
||||
const progressInterval = setInterval(() => {
|
||||
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||
if (progress < 90) {
|
||||
progress += Math.random() * 3;
|
||||
progressBar.style.width = Math.min(progress, 90) + '%';
|
||||
}
|
||||
progressStatus.textContent = `Re-ewaluacja newsów z Via Pomerania, NORDA... (${elapsed}s)`;
|
||||
}, 500);
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 180000);
|
||||
|
||||
const response = await fetch('/admin/zopk/news/reevaluate-low-scores', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ limit: 50 }),
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
clearInterval(progressInterval);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
progressBar.style.width = '100%';
|
||||
|
||||
document.getElementById('aiEvalProgress').style.display = 'none';
|
||||
document.getElementById('aiEvalResult').style.display = 'block';
|
||||
|
||||
const resultIcon = document.getElementById('aiResultIcon');
|
||||
const resultTitle = document.getElementById('aiResultTitle');
|
||||
const resultStats = document.getElementById('aiResultStats');
|
||||
|
||||
if (data.success) {
|
||||
resultIcon.textContent = '🔄';
|
||||
resultIcon.className = 'modal-icon success';
|
||||
resultTitle.textContent = 'Re-ewaluacja zakończona!';
|
||||
|
||||
// Build details list if available
|
||||
let detailsHtml = '';
|
||||
if (data.details && data.details.length > 0) {
|
||||
detailsHtml = `
|
||||
<div style="max-height: 200px; overflow-y: auto; margin-top: var(--spacing-md); text-align: left; font-size: var(--font-size-sm);">
|
||||
${data.details.map(d => `
|
||||
<div style="padding: 4px 0; border-bottom: 1px solid var(--border);">
|
||||
<span style="color: ${d.change === 'upgraded' ? 'var(--success)' : d.change === 'downgraded' ? 'var(--danger)' : 'var(--text-secondary)'};">
|
||||
${d.old_score}★ → ${d.new_score}★
|
||||
</span>
|
||||
${d.title}...
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
resultStats.innerHTML = `
|
||||
<div style="display: flex; gap: 20px; justify-content: center; margin-bottom: var(--spacing-md);">
|
||||
<div><strong>${data.total_evaluated}</strong><br><small>Ocenionych</small></div>
|
||||
<div style="color: var(--success);"><strong>${data.upgraded}</strong><br><small>⬆️ Podwyższono</small></div>
|
||||
<div style="color: var(--danger);"><strong>${data.downgraded}</strong><br><small>⬇️ Obniżono</small></div>
|
||||
<div style="color: var(--text-secondary);"><strong>${data.unchanged}</strong><br><small>Bez zmian</small></div>
|
||||
</div>
|
||||
<p style="text-align: center; color: var(--text-secondary);">${data.message}</p>
|
||||
${detailsHtml}
|
||||
`;
|
||||
|
||||
if (data.upgraded > 0 || data.downgraded > 0) {
|
||||
setTimeout(() => location.reload(), 3000);
|
||||
}
|
||||
} else {
|
||||
resultIcon.textContent = '✗';
|
||||
resultIcon.className = 'modal-icon error';
|
||||
resultTitle.textContent = 'Wystąpił błąd';
|
||||
resultStats.innerHTML = `<p style="text-align: center; color: var(--danger);">${data.error}</p>`;
|
||||
btn.disabled = false;
|
||||
btn.querySelector('.stat-label').textContent = 'Re-ewaluuj niskie oceny';
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(progressInterval);
|
||||
document.getElementById('aiEvalProgress').style.display = 'none';
|
||||
document.getElementById('aiEvalResult').style.display = 'block';
|
||||
|
||||
const resultIcon = document.getElementById('aiResultIcon');
|
||||
const resultTitle = document.getElementById('aiResultTitle');
|
||||
const resultStats = document.getElementById('aiResultStats');
|
||||
|
||||
resultIcon.textContent = '✗';
|
||||
resultIcon.className = 'modal-icon error';
|
||||
resultTitle.textContent = 'Błąd połączenia';
|
||||
resultStats.innerHTML = `<p style="text-align: center; color: var(--danger);">${error.message}</p>`;
|
||||
|
||||
btn.disabled = false;
|
||||
btn.querySelector('.stat-label').textContent = 'Re-ewaluuj niskie oceny';
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal on click outside
|
||||
document.getElementById('aiEvalModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
|
||||
@ -1426,6 +1426,143 @@ def reevaluate_news_without_score(db_session, limit: int = 50, user_id: int = No
|
||||
return stats
|
||||
|
||||
|
||||
def reevaluate_low_score_news(db_session, limit: int = 50, user_id: int = None) -> Dict:
|
||||
"""
|
||||
Re-evaluate news with low AI scores (1-2★) that contain key ZOPK topics.
|
||||
|
||||
This is useful after updating the AI prompt to include new topics like
|
||||
Via Pomerania, S6, NORDA, etc. Old articles may have been scored low
|
||||
before these topics were added to the prompt.
|
||||
|
||||
Key topics that trigger re-evaluation:
|
||||
- Via Pomerania (droga ekspresowa)
|
||||
- S6, S7 (autostrady)
|
||||
- Droga Czerwona
|
||||
- NORDA, Izba Przedsiębiorców
|
||||
- Pakt Bezpieczeństwa
|
||||
- Deklaracja Bałtycka
|
||||
|
||||
Args:
|
||||
db_session: SQLAlchemy session
|
||||
limit: Max number of items to evaluate (to avoid API limits)
|
||||
user_id: User triggering the evaluation (for logging)
|
||||
|
||||
Returns:
|
||||
Dict with stats: total_evaluated, upgraded, downgraded, unchanged, errors
|
||||
"""
|
||||
from database import ZOPKNews
|
||||
from datetime import datetime
|
||||
from sqlalchemy import or_, func
|
||||
|
||||
# Key topics that should trigger re-evaluation
|
||||
KEY_TOPICS = [
|
||||
'via pomerania', 'via-pomerania',
|
||||
'droga czerwona',
|
||||
'pakt bezpieczeństwa', 'pakt dla bezpieczeństwa',
|
||||
'deklaracja bałtycka',
|
||||
'norda biznes', 'izba przedsiębiorców norda', 'akademia biznesu norda',
|
||||
's6 koszalin', 's6 słupsk',
|
||||
's7 gdańsk', 's7 elbląg',
|
||||
]
|
||||
|
||||
# Build filter for news with low scores containing key topics
|
||||
topic_filters = []
|
||||
for topic in KEY_TOPICS:
|
||||
topic_filters.append(func.lower(ZOPKNews.title).contains(topic))
|
||||
topic_filters.append(func.lower(ZOPKNews.description).contains(topic))
|
||||
|
||||
# Get news with score 1-2 that contain key topics
|
||||
news_to_rescore = db_session.query(ZOPKNews).filter(
|
||||
ZOPKNews.ai_relevance_score.isnot(None), # Has been evaluated
|
||||
ZOPKNews.ai_relevance_score <= 2, # Low score (1-2★)
|
||||
or_(*topic_filters) # Contains key topics
|
||||
).order_by(ZOPKNews.created_at.desc()).limit(limit).all()
|
||||
|
||||
if not news_to_rescore:
|
||||
return {
|
||||
'total_evaluated': 0,
|
||||
'upgraded': 0,
|
||||
'downgraded': 0,
|
||||
'unchanged': 0,
|
||||
'errors': 0,
|
||||
'message': 'Brak newsów z niską oceną zawierających kluczowe tematy'
|
||||
}
|
||||
|
||||
# Get Gemini service once
|
||||
try:
|
||||
from gemini_service import get_gemini_service
|
||||
gemini = get_gemini_service()
|
||||
except Exception as e:
|
||||
return {
|
||||
'total_evaluated': 0,
|
||||
'upgraded': 0,
|
||||
'downgraded': 0,
|
||||
'unchanged': 0,
|
||||
'errors': 1,
|
||||
'message': f'Gemini service error: {str(e)}'
|
||||
}
|
||||
|
||||
stats = {
|
||||
'total_evaluated': 0,
|
||||
'upgraded': 0,
|
||||
'downgraded': 0,
|
||||
'unchanged': 0,
|
||||
'errors': 0,
|
||||
'details': [] # Track individual changes
|
||||
}
|
||||
|
||||
for news in news_to_rescore:
|
||||
old_score = news.ai_relevance_score
|
||||
result = evaluate_news_relevance(news, gemini, user_id=user_id)
|
||||
|
||||
if result['evaluated']:
|
||||
new_score = result.get('score', old_score)
|
||||
|
||||
# Update the news item
|
||||
news.ai_relevant = result['relevant']
|
||||
news.ai_relevance_score = new_score
|
||||
news.ai_evaluation_reason = result['reason']
|
||||
news.ai_evaluated_at = datetime.now()
|
||||
news.ai_model = 'gemini-2.5-flash-lite' # Updated model name
|
||||
|
||||
# Track change
|
||||
stats['total_evaluated'] += 1
|
||||
if new_score > old_score:
|
||||
stats['upgraded'] += 1
|
||||
# If score is now >= 3, auto-approve
|
||||
if new_score >= 3 and news.status in ('pending', 'rejected'):
|
||||
news.status = 'auto_approved'
|
||||
news.is_auto_verified = True
|
||||
elif new_score < old_score:
|
||||
stats['downgraded'] += 1
|
||||
else:
|
||||
stats['unchanged'] += 1
|
||||
|
||||
stats['details'].append({
|
||||
'id': news.id,
|
||||
'title': news.title[:50],
|
||||
'old_score': old_score,
|
||||
'new_score': new_score,
|
||||
'change': 'upgraded' if new_score > old_score else ('downgraded' if new_score < old_score else 'unchanged')
|
||||
})
|
||||
|
||||
logger.info(f"Re-evaluated news {news.id}: {old_score}★ → {new_score}★")
|
||||
else:
|
||||
stats['errors'] += 1
|
||||
logger.warning(f"Failed to re-evaluate news {news.id}: {result['reason']}")
|
||||
|
||||
# Commit all changes
|
||||
try:
|
||||
db_session.commit()
|
||||
stats['message'] = f"Re-ewaluacja {stats['total_evaluated']} newsów: {stats['upgraded']} podwyższono, {stats['downgraded']} obniżono, {stats['unchanged']} bez zmian"
|
||||
except Exception as e:
|
||||
db_session.rollback()
|
||||
stats['errors'] += 1
|
||||
stats['message'] = f'Database error: {str(e)}'
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def evaluate_pending_news(db_session, limit: int = 50, user_id: int = None) -> Dict:
|
||||
"""
|
||||
Evaluate multiple pending news items for ZOPK relevance.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user