Replace ~170 manual `if not current_user.is_admin` checks with: - @role_required(SystemRole.ADMIN) for user management, security, ZOPK - @role_required(SystemRole.OFFICE_MANAGER) for content management - current_user.can_access_admin_panel() for admin UI access - current_user.can_moderate_forum() for forum moderation - current_user.can_edit_company(id) for company permissions Add @office_manager_required decorator shortcut. Add SQL migration to sync existing users' role field. Role hierarchy: UNAFFILIATED(10) < MEMBER(20) < EMPLOYEE(30) < MANAGER(40) < OFFICE_MANAGER(50) < ADMIN(100) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
459 lines
20 KiB
Python
459 lines
20 KiB
Python
"""
|
|
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,
|
|
SystemRole,
|
|
ZOPKNews,
|
|
)
|
|
from utils.decorators import role_required
|
|
|
|
from . import bp
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@bp.route('/model-comparison')
|
|
@login_required
|
|
@role_required(SystemRole.OFFICE_MANAGER)
|
|
def admin_model_comparison():
|
|
"""Admin page for comparing AI model responses"""
|
|
|
|
# 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
|
|
@role_required(SystemRole.OFFICE_MANAGER)
|
|
def admin_model_comparison_run():
|
|
"""Run model comparison simulation"""
|
|
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
|