refactor: Move admin_model_comparison routes to admin blueprint

- Created routes_model_comparison.py with model comparison functionality
- Updated base.html to use full blueprint name
- Added aliases for backward compatibility
- Commented old routes in app.py with _old_ prefix

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-01-31 10:48:18 +01:00
parent 532d666fa8
commit 351c8fba75
5 changed files with 471 additions and 7 deletions

12
app.py
View File

@ -4537,9 +4537,9 @@ def _old_api_ai_learning_status():
# MODEL COMPARISON - Porównanie modeli AI
# ============================================================
@app.route('/admin/model-comparison')
@login_required
def admin_model_comparison():
# @app.route('/admin/model-comparison') # MOVED TO admin.admin_model_comparison
# @login_required
def _old_admin_model_comparison():
"""Admin page for comparing AI model responses"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
@ -4567,9 +4567,9 @@ def admin_model_comparison():
)
@app.route('/admin/model-comparison/run', methods=['POST'])
@login_required
def admin_model_comparison_run():
# @app.route('/admin/model-comparison/run', methods=['POST']) # MOVED TO admin.admin_model_comparison_run
# @login_required
def _old_admin_model_comparison_run():
"""Run model comparison simulation"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403

View File

@ -267,6 +267,9 @@ def register_blueprints(app):
# AI Usage Monitoring (Phase 6.2b)
'admin_ai_usage': 'admin.admin_ai_usage',
'admin_ai_usage_user': 'admin.admin_ai_usage_user',
# Model Comparison (Phase 6.2b)
'admin_model_comparison': 'admin.admin_model_comparison',
'admin_model_comparison_run': 'admin.admin_model_comparison_run',
})
logger.info("Created admin endpoint aliases")
except ImportError as e:

View File

@ -17,3 +17,4 @@ from . import routes_security # noqa: E402, F401
from . import routes_announcements # noqa: E402, F401
from . import routes_insights # noqa: E402, F401
from . import routes_analytics # noqa: E402, F401
from . import routes_model_comparison # noqa: E402, F401

View File

@ -0,0 +1,460 @@
"""
Admin Blueprint - Model Comparison Routes
==========================================
Routes for comparing AI model responses (Gemini).
"""
import json
import logging
import os
from datetime import date, datetime, timedelta
from flask import flash, jsonify, redirect, render_template, request, url_for
from flask_login import current_user, login_required
from sqlalchemy import func
from sqlalchemy.orm import joinedload
import gemini_service
from database import (
Category,
Classified,
Company,
CompanyPerson,
CompanyRecommendation,
CompanySocialMedia,
CompanyWebsiteAnalysis,
ForumTopic,
GBPAudit,
NordaEvent,
SessionLocal,
ZOPKNews,
)
from . import bp
logger = logging.getLogger(__name__)
@bp.route('/model-comparison')
@login_required
def admin_model_comparison():
"""Admin page for comparing AI model responses"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('dashboard'))
# Load saved comparison results if exist
results_file = '/tmp/nordabiz_model_comparison_results.json'
results = None
generated_at = None
if os.path.exists(results_file):
try:
with open(results_file, 'r', encoding='utf-8') as f:
data = json.load(f)
results = data.get('results', {})
generated_at = data.get('generated_at', 'Nieznana data')
except Exception as e:
logger.error(f"Error loading model comparison results: {e}")
return render_template(
'admin/model_comparison.html',
results=results,
generated_at=generated_at
)
@bp.route('/model-comparison/run', methods=['POST'])
@login_required
def admin_model_comparison_run():
"""Run model comparison simulation"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
try:
# Questions to compare (from real conversations)
comparison_questions = {
'artur': {
'user_id': 3,
'user_name': 'Artur Wiertel',
'original_conversation_id': 63,
'comparisons': [
{
'question_id': 1,
'question': 'ilu członków ma izba norda?',
'type': 'simple',
'old_model': 'gemini-2.5-flash-lite',
'old_answer': 'Izba Norda Biznes zrzesza obecnie 111 firm.',
'new_model': 'gemini-3-flash-preview',
'new_answer': None
},
{
'question_id': 2,
'question': 'czy w izbie są osoby gotowe do współpracy przy wspólnych projektach inwestycyjnych?',
'type': 'complex',
'old_model': 'gemini-2.5-flash-lite',
'old_answer': 'W Norda Biznes posiadamy firmy otwarte na współpracę przy projektach inwestycyjnych. Szczególnie interesujące mogą być podmioty z kategorii Budownictwo, Nieruchomości oraz Usługi Biznesowe. Czy szukasz konkretnego rodzaju partnera lub projektu?',
'new_model': 'gemini-3-flash-preview',
'new_answer': None
},
{
'question_id': 3,
'question': 'Mam pomysł i projekt ciekawego przedsięwzięcia inwestycyjnego - budowa nieruchomości komercyjnej. Szukam partnerów - 3 osoby, wkład na poziomie 1,5 mln netto',
'type': 'business',
'old_model': 'gemini-2.5-flash-lite',
'old_answer': 'Poszukujesz partnerów do inwestycji w nieruchomość komercyjną. W naszej bazie znajdują się firmy, które mogą być zainteresowane takim przedsięwzięciem. Warto zwrócić uwagę na: ARD Invest, Progress Optima, Orlex MG...',
'new_model': 'gemini-3-flash-preview',
'new_answer': None
},
{
'question_id': 4,
'question': 'a co sądzisz o firmie sigma?',
'type': 'company',
'old_model': 'gemini-2.5-flash-lite',
'old_answer': 'Firma Sigma Budownictwo to generalny wykonawca robót budowlanych, który ma na swoim koncie portfolio ponad 250 tys. m² powierzchni użytkowej, w tym hale przemysłowe. Jest to rodzinna firma z Wejherowa, zarządzana przez Romana i Tomasza Wiercińskich.',
'new_model': 'gemini-3-flash-preview',
'new_answer': None
},
{
'question_id': 5,
'question': 'co myślisz o pomysłach Jacka Pomieczyńskiego na forum?',
'type': 'analysis',
'old_model': 'gemini-2.5-flash-lite',
'old_answer': 'Chętnie pomogę! Aby odpowiedzieć na Twoje pytanie o pomysły Jacka Pomieczyńskiego, potrzebuję więcej informacji. Czy możesz sprecyzować, o jakie konkretnie pomysły chodzi?',
'new_model': 'gemini-3-flash-preview',
'new_answer': None
}
]
},
'maciej': {
'user_id': 1,
'user_name': 'Maciej Pienczyn',
'original_conversation_id': 62,
'comparisons': [
{
'question_id': 1,
'question': 'Kto jest prezesem PIXLAB?',
'type': 'simple',
'old_model': 'gemini-2.5-flash-lite',
'old_answer': 'Prezesem firmy Pixlab Softwarehouse jest Michał Bogdan Roszman.',
'new_model': 'gemini-3-flash-preview',
'new_answer': None
},
{
'question_id': 2,
'question': 'jakie znasz inne firmy IT w Norda?',
'type': 'listing',
'old_model': 'gemini-2.5-flash-lite',
'old_answer': 'W Norda Biznes działa kilka firm z branży IT i Telekomunikacja. Są to między innymi VENCODE, oferujące dedykowane aplikacje oparte na sztucznej inteligencji i technologii blockchain, oraz Delkom, specjalizujący się w usługach IT, cyberbezpieczeństwie i doradztwie informatycznym.',
'new_model': 'gemini-3-flash-preview',
'new_answer': None
},
{
'question_id': 3,
'question': 'podaj osoby kontaktowe - będę budował sieć komputerową',
'type': 'contact',
'old_model': 'gemini-2.5-flash-lite',
'old_answer': 'Wśród firm z branży IT i telekomunikacja, które mogą pomóc w budowie sieci komputerowej, znajdują się: Chopin Telewizja Kablowa: Bogdan Łaga (Prezes), Delkom: Seweryn Kędziora (Członek Zarządu), INPI Sp. z o.o.',
'new_model': 'gemini-3-flash-preview',
'new_answer': None
}
]
}
}
# Use gemini_service directly with FULL NordaGPT context
gs = gemini_service.get_gemini_service()
logger.info(f"Running model comparison with: {gs.model_name}")
# Build FULL context from database (like NordaBizChatEngine._build_conversation_context)
db = SessionLocal()
try:
# === ALL COMPANIES in compact format ===
all_companies = db.query(Company).filter_by(status='active').all()
companies_list = []
for c in all_companies:
info = f"- {c.name}"
if c.category:
info += f" [{c.category.name}]"
if c.description_short:
info += f": {c.description_short}"
if c.founding_history:
info += f" Historia: {c.founding_history[:200]}"
services = [cs.service.name for cs in c.services if cs.service] if c.services else []
if services:
info += f" Usługi: {', '.join(services[:5])}"
if c.website:
info += f" WWW: {c.website}"
if c.phone:
info += f" Tel: {c.phone}"
if c.email:
info += f" Email: {c.email}"
if c.address_city:
info += f" Miasto: {c.address_city}"
info += f" Profil: https://nordabiznes.pl/company/{c.slug}"
companies_list.append(info)
companies_context = "\n".join(companies_list)
total_count = len(all_companies)
# === CATEGORIES with counts ===
categories = db.query(Category).all()
categories_context = "Kategorie firm:\n" + "\n".join([
f"- {cat.name}: {db.query(Company).filter_by(category_id=cat.id, status='active').count()} firm"
for cat in categories
])
# === COMPANY PEOPLE (zarząd, wspólnicy) ===
company_people = db.query(CompanyPerson).options(
joinedload(CompanyPerson.person),
joinedload(CompanyPerson.company)
).all()
people_lines = []
for cp in company_people:
if cp.company and cp.person:
line = f"- {cp.company.name}: {cp.person.full_name()}"
if cp.role:
line += f" ({cp.role})"
if cp.shares_percent:
line += f" - {cp.shares_percent}% udziałów"
people_lines.append(line)
people_context = "Osoby w firmach (zarząd, wspólnicy):\n" + "\n".join(people_lines) if people_lines else ""
# === RECOMMENDATIONS ===
recommendations = db.query(CompanyRecommendation).filter_by(
status='approved'
).order_by(CompanyRecommendation.created_at.desc()).limit(20).all()
recs_lines = []
for rec in recommendations:
if rec.company:
line = f"- {rec.company.name}: {rec.recommendation_text[:150] if rec.recommendation_text else ''}"
recs_lines.append(line)
recommendations_context = "Rekomendacje firm:\n" + "\n".join(recs_lines) if recs_lines else ""
# === FORUM TOPICS ===
forum_topics = db.query(ForumTopic).filter(
ForumTopic.category != 'test'
).order_by(ForumTopic.created_at.desc()).limit(15).all()
forum_lines = []
for topic in forum_topics:
line = f"- [{topic.category_label}] {topic.title}"
if topic.reply_count:
line += f" ({topic.reply_count} odpowiedzi)"
forum_lines.append(line)
forum_context = "Tematy na forum:\n" + "\n".join(forum_lines) if forum_lines else ""
# === UPCOMING EVENTS ===
event_cutoff = date.today() + timedelta(days=60)
upcoming_events = db.query(NordaEvent).filter(
NordaEvent.event_date >= date.today(),
NordaEvent.event_date <= event_cutoff
).order_by(NordaEvent.event_date).limit(15).all()
events_lines = []
for event in upcoming_events:
line = f"- {event.event_date.strftime('%Y-%m-%d')}: {event.title}"
if event.location:
line += f" ({event.location})"
events_lines.append(line)
events_context = "Nadchodzące wydarzenia:\n" + "\n".join(events_lines) if events_lines else ""
# === B2B CLASSIFIEDS ===
active_classifieds = db.query(Classified).filter(
Classified.is_active == True,
Classified.is_test == False
).order_by(Classified.created_at.desc()).limit(20).all()
classifieds_lines = []
for c in active_classifieds:
line = f"- [{c.listing_type}] {c.title}"
if c.company:
line += f" - {c.company.name}"
classifieds_lines.append(line)
classifieds_context = "Ogłoszenia B2B:\n" + "\n".join(classifieds_lines) if classifieds_lines else ""
# === RECENT NEWS (ZOPK) ===
news_cutoff = datetime.now() - timedelta(days=30)
recent_news = db.query(ZOPKNews).filter(
ZOPKNews.status.in_(['approved', 'auto_approved']),
ZOPKNews.published_at >= news_cutoff
).order_by(ZOPKNews.published_at.desc()).limit(10).all()
news_lines = []
for news in recent_news:
line = f"- {news.published_at.strftime('%Y-%m-%d') if news.published_at else ''}: {news.title}"
news_lines.append(line)
news_context = "Ostatnie aktualności:\n" + "\n".join(news_lines) if news_lines else ""
# === SOCIAL MEDIA ===
social_media = db.query(CompanySocialMedia).filter(
CompanySocialMedia.is_valid == True
).options(joinedload(CompanySocialMedia.company)).all()
social_lines = []
for sm in social_media:
if sm.company:
line = f"- {sm.company.name}: {sm.platform}"
if sm.followers_count:
line += f" ({sm.followers_count} obserwujących)"
social_lines.append(line)
social_context = "Social media firm:\n" + "\n".join(social_lines[:30]) if social_lines else ""
# === GBP AUDITS (Google Business Profile) ===
# Get latest audit per company
latest_audit_subq = db.query(
GBPAudit.company_id,
func.max(GBPAudit.audit_date).label('max_date')
).group_by(GBPAudit.company_id).subquery()
latest_audits = db.query(GBPAudit).join(
latest_audit_subq,
(GBPAudit.company_id == latest_audit_subq.c.company_id) &
(GBPAudit.audit_date == latest_audit_subq.c.max_date)
).options(joinedload(GBPAudit.company)).all()
gbp_lines = []
for audit in latest_audits:
if audit.company:
line = f"- {audit.company.name}: Kompletność {audit.completeness_score or 0}%"
if audit.review_count:
line += f", {audit.review_count} recenzji"
if audit.average_rating:
line += f", ocena {float(audit.average_rating):.1f}/5"
if audit.google_maps_url:
line += f" Maps: {audit.google_maps_url}"
gbp_lines.append(line)
gbp_context = "Audyty Google Business Profile:\n" + "\n".join(gbp_lines) if gbp_lines else ""
# === SEO AUDITS (PageSpeed) ===
seo_audits = db.query(CompanyWebsiteAnalysis).filter(
CompanyWebsiteAnalysis.pagespeed_seo_score.isnot(None)
).options(joinedload(CompanyWebsiteAnalysis.company)).all()
seo_lines = []
for audit in seo_audits:
if audit.company:
line = f"- {audit.company.name}: SEO {audit.pagespeed_seo_score or 0}/100"
if audit.pagespeed_performance_score:
line += f", Wydajność {audit.pagespeed_performance_score}/100"
if audit.pagespeed_accessibility_score:
line += f", Dostępność {audit.pagespeed_accessibility_score}/100"
if audit.seo_overall_score:
line += f", Ogólnie {audit.seo_overall_score}/100"
seo_lines.append(line)
seo_context = "Audyty SEO stron WWW:\n" + "\n".join(seo_lines) if seo_lines else ""
# === ZOPK KNOWLEDGE BASE ===
zopk_knowledge = """Baza wiedzy ZOPK (Zielony Okręg Przemysłowy Kaszubia):
ELEKTROWNIA JĄDROWA:
- Lokalizacja: Lubiatowo-Kopalino (gmina Choczewo)
- Inwestor: Polskie Elektrownie Jądrowe (PEJ)
- Partner technologiczny: Westinghouse (reaktory AP1000)
- Moc: 2 reaktory po 1150 MW (łącznie 2300 MW)
- Harmonogram: Budowa 2028-2035, uruchomienie 2035-2037
- Zatrudnienie: 3000 miejsc pracy (budowa), 900 stałych (eksploatacja)
OFFSHORE WIND (Morskie Farmy Wiatrowe):
- Baltic Power (Orlen + Northland): 1.2 GW, 76 turbin, ~25 km od Łeby
- Baltica 2 (PGE + Ørsted): 1.5 GW, na wschód od Łeby
- Baltica 3 (PGE + Ørsted): 1 GW
- Łączna moc planowana: 5.9 GW do 2030, 11 GW do 2040
- Port serwisowy: Ustka, Łeba (rozbudowa)
INFRASTRUKTURA TRANSPORTOWA:
- Via Pomerania: Droga S6 Szczecin-Gdańsk (w budowie)
- Droga Czerwona: S7 Gdańsk-Elbląg z Obwodnicą Metropolitalną
- PKM (Pomorska Kolej Metropolitalna): Rozwój sieci
INWESTYCJE PRZEMYSŁOWE:
- Kongsberg Maritime: Fabryka w Rumi (automatyzacja morska)
- Bałtycki Port Nowych Technologii: Gdynia
- Pomorska Specjalna Strefa Ekonomiczna: Ulgi podatkowe
IZBA NORDA BIZNES:
- Siedziba: Wejherowo
- Członkowie: 150 firm
- Cel: Networking, współpraca biznesowa, rozwój regionu"""
finally:
db.close()
# Build comprehensive system prompt with ALL context
system_prompt = f"""Jesteś NordaGPT - inteligentnym asystentem portalu Norda Biznes, katalogu {total_count} firm zrzeszonych w stowarzyszeniu Norda Biznes z Wejherowa (Polska).
TWOJE MOŻLIWOŚCI:
- Znasz WSZYSTKIE firmy członkowskie, ich dane kontaktowe, usługi, historię
- Znasz osoby zarządzające firmami (prezesi, wspólnicy, udziałowcy)
- Śledzisz aktualności, wydarzenia i ogłoszenia B2B
- Możesz polecić firmy do współpracy na podstawie potrzeb użytkownika
- Śledzisz dyskusje na forum członków
- Znasz wyniki audytów Google Business Profile (oceny, recenzje)
- Znasz wyniki audytów SEO stron WWW firm
- Masz wiedzę o ZOPK (Zielony Okręg Przemysłowy Kaszubia) - elektrownia jądrowa, offshore wind, infrastruktura
=== BAZA FIRM ({total_count} aktywnych) ===
{companies_context}
=== {categories_context} ===
{people_context}
{recommendations_context}
{forum_context}
{events_context}
{classifieds_context}
{news_context}
{social_context}
{gbp_context}
{seo_context}
{zopk_knowledge}
=== ZASADY ODPOWIEDZI ===
- Odpowiadaj konkretnie, podając nazwy firm i dane kontaktowe
- Linkuj do profili firm na portalu: https://nordabiznes.pl/company/[slug]
- Jeśli pytanie dotyczy konkretnej firmy - podaj szczegóły z bazy
- Przy pytaniach o osoby - podaj stanowisko i firmę
- NIE podawaj danych kontaktowych osób, które je ukryły w ustawieniach prywatności
- Bądź pomocny i profesjonalny"""
# Generate new responses
for user_key, user_data in comparison_questions.items():
for comp in user_data['comparisons']:
try:
prompt = f"{system_prompt}\n\nPytanie użytkownika: {comp['question']}"
response_text = gs.generate_text(prompt=prompt, temperature=0.7)
comp['new_answer'] = response_text if response_text else 'Brak odpowiedzi'
logger.info(f"Generated response for {user_key} Q{comp['question_id']}")
except Exception as e:
comp['new_answer'] = f'Błąd: {str(e)}'
logger.error(f"Error generating response for {user_key} Q{comp['question_id']}: {e}")
# Save results to /tmp (always writable)
results_file = '/tmp/nordabiz_model_comparison_results.json'
with open(results_file, 'w', encoding='utf-8') as f:
json.dump({
'generated_at': datetime.now().strftime('%Y-%m-%d %H:%M'),
'old_model': 'gemini-2.5-flash-lite',
'new_model': 'gemini-3-flash-preview',
'results': comparison_questions
}, f, ensure_ascii=False, indent=2)
return jsonify({'success': True})
except Exception as e:
logger.error(f"Error running model comparison: {e}")
return jsonify({'success': False, 'error': str(e)}), 500

View File

@ -1347,7 +1347,7 @@
</svg>
Monitoring AI
</a>
<a href="{{ url_for('admin_model_comparison') }}">
<a href="{{ url_for('admin.admin_model_comparison') }}">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2"/>
</svg>