Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Replace ~20 remaining is_admin references across backend, templates and scripts with proper SystemRole checks. Column is_admin stays as deprecated (synced by set_role()) until DB migration removes it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1011 lines
35 KiB
Python
1011 lines
35 KiB
Python
"""
|
|
Company API Routes - API blueprint
|
|
|
|
Migrated from app.py as part of the blueprint refactoring.
|
|
Contains API routes for company data, validation, and AI enrichment.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import time
|
|
from datetime import datetime
|
|
|
|
import requests
|
|
from bs4 import BeautifulSoup
|
|
from flask import jsonify, request, current_app
|
|
from flask_login import current_user, login_required
|
|
|
|
from database import (
|
|
SessionLocal, Company, User, Person, CompanyPerson, CompanyAIInsights, AiEnrichmentProposal
|
|
)
|
|
from datetime import timedelta
|
|
import gemini_service
|
|
import krs_api_service
|
|
from . import bp
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================
|
|
# COMPANY DATA API ROUTES
|
|
# ============================================================
|
|
|
|
@bp.route('/companies')
|
|
def api_companies():
|
|
"""API: Get all companies"""
|
|
db = SessionLocal()
|
|
try:
|
|
companies = db.query(Company).filter_by(status='active').all()
|
|
return jsonify({
|
|
'success': True,
|
|
'companies': [
|
|
{
|
|
'id': c.id,
|
|
'name': c.name,
|
|
'category': c.category.name if c.category else None,
|
|
'description': c.description_short,
|
|
'website': c.website,
|
|
'phone': c.phone,
|
|
'email': c.email
|
|
}
|
|
for c in companies
|
|
]
|
|
})
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/connections')
|
|
def api_connections():
|
|
"""
|
|
API: Get company-person connections for D3.js visualization.
|
|
Returns nodes (companies and people) and links (relationships).
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
# Get all companies with people data
|
|
companies = db.query(Company).filter_by(status='active').all()
|
|
|
|
# Get all people with company relationships
|
|
people = db.query(Person).join(CompanyPerson).distinct().all()
|
|
|
|
# Build nodes
|
|
nodes = []
|
|
|
|
# Company nodes
|
|
for c in companies:
|
|
nodes.append({
|
|
'id': f'company_{c.id}',
|
|
'name': c.name,
|
|
'type': 'company',
|
|
'category': c.category.name if c.category else 'Other',
|
|
'slug': c.slug,
|
|
'has_krs': bool(c.krs),
|
|
'city': c.address_city or ''
|
|
})
|
|
|
|
# Person nodes
|
|
for p in people:
|
|
# Count UNIQUE companies this person is connected to (not roles)
|
|
company_count = len(set(r.company_id for r in p.company_roles if r.company and r.company.status == 'active'))
|
|
nodes.append({
|
|
'id': f'person_{p.id}',
|
|
'name': f'{p.imiona} {p.nazwisko}',
|
|
'type': 'person',
|
|
'company_count': company_count
|
|
})
|
|
|
|
# Build links
|
|
links = []
|
|
for p in people:
|
|
for role in p.company_roles:
|
|
if role.company and role.company.status == 'active':
|
|
links.append({
|
|
'source': f'person_{p.id}',
|
|
'target': f'company_{role.company_id}',
|
|
'role': role.role,
|
|
'category': role.role_category
|
|
})
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'nodes': nodes,
|
|
'links': links,
|
|
'stats': {
|
|
'companies': len([n for n in nodes if n['type'] == 'company']),
|
|
'people': len([n for n in nodes if n['type'] == 'person']),
|
|
'connections': len(links)
|
|
}
|
|
})
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# ============================================================
|
|
# VALIDATION API ROUTES
|
|
# ============================================================
|
|
|
|
def validate_email(email):
|
|
"""Simple email validation"""
|
|
import re
|
|
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
return re.match(pattern, email) is not None
|
|
|
|
|
|
@bp.route('/check-email', methods=['POST'])
|
|
def api_check_email():
|
|
"""API: Check if email is available"""
|
|
data = request.get_json()
|
|
email = data.get('email', '').strip().lower()
|
|
|
|
# Validate email format
|
|
if not email or not validate_email(email):
|
|
return jsonify({
|
|
'available': False,
|
|
'error': 'Nieprawidłowy format email'
|
|
}), 400
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
# Check if email exists
|
|
existing_user = db.query(User).filter_by(email=email).first()
|
|
|
|
return jsonify({
|
|
'available': existing_user is None,
|
|
'email': email
|
|
})
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/verify-nip', methods=['POST'])
|
|
def api_verify_nip():
|
|
"""API: Verify NIP and check if company is NORDA member"""
|
|
data = request.get_json()
|
|
nip = data.get('nip', '').strip()
|
|
|
|
# Validate NIP format
|
|
if not nip or not re.match(r'^\d{10}$', nip):
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Nieprawidłowy format NIP'
|
|
}), 400
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
# Check if NIP exists in companies database
|
|
company = db.query(Company).filter_by(nip=nip, status='active').first()
|
|
|
|
if company:
|
|
return jsonify({
|
|
'success': True,
|
|
'is_member': True,
|
|
'company_name': company.name,
|
|
'company_id': company.id
|
|
})
|
|
else:
|
|
return jsonify({
|
|
'success': True,
|
|
'is_member': False,
|
|
'company_name': None,
|
|
'company_id': None
|
|
})
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/verify-krs', methods=['GET', 'POST'])
|
|
def api_verify_krs():
|
|
"""
|
|
API: Verify company data from KRS Open API (prs.ms.gov.pl).
|
|
|
|
GET /api/verify-krs?krs=0000817317
|
|
POST /api/verify-krs with JSON body: {"krs": "0000817317"}
|
|
|
|
Returns official KRS data including:
|
|
- Company name, NIP, REGON
|
|
- Address
|
|
- Capital
|
|
- Registration date
|
|
- Management board (anonymized in Open API)
|
|
- Shareholders (anonymized in Open API)
|
|
"""
|
|
# Get KRS from query params (GET) or JSON body (POST)
|
|
if request.method == 'GET':
|
|
krs = request.args.get('krs', '').strip()
|
|
else:
|
|
data = request.get_json(silent=True) or {}
|
|
krs = data.get('krs', '').strip()
|
|
|
|
# Validate KRS format (7-10 digits)
|
|
if not krs or not re.match(r'^\d{7,10}$', krs):
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Nieprawidłowy format KRS (wymagane 7-10 cyfr)'
|
|
}), 400
|
|
|
|
# Normalize to 10 digits
|
|
krs_normalized = krs.zfill(10)
|
|
|
|
try:
|
|
# Fetch data from KRS Open API
|
|
krs_data = krs_api_service.get_company_from_krs(krs_normalized)
|
|
|
|
if krs_data is None:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Nie znaleziono podmiotu o KRS {krs_normalized} w rejestrze',
|
|
'krs': krs_normalized
|
|
}), 404
|
|
|
|
# Check if company exists in our database
|
|
db = SessionLocal()
|
|
try:
|
|
our_company = db.query(Company).filter_by(krs=krs_normalized).first()
|
|
is_member = our_company is not None
|
|
company_id = our_company.id if our_company else None
|
|
finally:
|
|
db.close()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'krs': krs_normalized,
|
|
'is_norda_member': is_member,
|
|
'company_id': company_id,
|
|
'data': krs_data.to_dict(),
|
|
'formatted_address': krs_api_service.format_address(krs_data),
|
|
'source': 'KRS Open API (prs.ms.gov.pl)',
|
|
'note': 'Dane osobowe (imiona, nazwiska) są zanonimizowane w Open API'
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Błąd podczas pobierania danych z KRS: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@bp.route('/company/<int:company_id>/refresh-krs', methods=['POST'])
|
|
@login_required
|
|
def api_refresh_company_krs(company_id):
|
|
"""
|
|
API: Refresh company data from KRS Open API.
|
|
Updates company record with official KRS data.
|
|
Requires login.
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
company = db.query(Company).filter_by(id=company_id).first()
|
|
|
|
if not company:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Firma nie znaleziona'
|
|
}), 404
|
|
|
|
if not company.krs:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Firma nie ma numeru KRS'
|
|
}), 400
|
|
|
|
# Fetch data from KRS
|
|
krs_data = krs_api_service.get_company_from_krs(company.krs)
|
|
|
|
if krs_data is None:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Nie znaleziono podmiotu o KRS {company.krs} w rejestrze'
|
|
}), 404
|
|
|
|
# Update company data (only non-personal data)
|
|
updates = {}
|
|
|
|
if krs_data.nip and krs_data.nip != company.nip:
|
|
updates['nip'] = krs_data.nip
|
|
company.nip = krs_data.nip
|
|
|
|
if krs_data.regon:
|
|
regon_9 = krs_data.regon[:9]
|
|
if regon_9 != company.regon:
|
|
updates['regon'] = regon_9
|
|
company.regon = regon_9
|
|
|
|
# Update address if significantly different
|
|
new_address = krs_api_service.format_address(krs_data)
|
|
if new_address and new_address != company.address:
|
|
updates['address'] = new_address
|
|
company.address = new_address
|
|
|
|
if krs_data.miejscowosc and krs_data.miejscowosc != company.city:
|
|
updates['city'] = krs_data.miejscowosc
|
|
company.city = krs_data.miejscowosc
|
|
|
|
if krs_data.kapital_zakladowy:
|
|
updates['kapital_zakladowy'] = krs_data.kapital_zakladowy
|
|
|
|
# Update verification timestamp
|
|
company.krs_verified_at = datetime.utcnow()
|
|
|
|
db.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'company_id': company_id,
|
|
'updates': updates,
|
|
'krs_data': krs_data.to_dict(),
|
|
'message': f'Zaktualizowano {len(updates)} pól' if updates else 'Dane są aktualne'
|
|
})
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Błąd podczas aktualizacji: {str(e)}'
|
|
}), 500
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# ============================================================
|
|
# AI ENRICHMENT HELPER FUNCTIONS
|
|
# ============================================================
|
|
|
|
def _search_brave_for_company(company_name: str, city: str = None) -> dict:
|
|
"""
|
|
Search Brave API for company information.
|
|
Returns dict with news items and web results.
|
|
"""
|
|
brave_api_key = os.getenv('BRAVE_API_KEY')
|
|
if not brave_api_key:
|
|
logger.warning("BRAVE_API_KEY not configured, skipping web search")
|
|
return {'news': [], 'web': []}
|
|
|
|
results = {'news': [], 'web': []}
|
|
|
|
# Build search query
|
|
query = f'"{company_name}"'
|
|
if city:
|
|
query += f' {city}'
|
|
|
|
try:
|
|
headers = {
|
|
'Accept': 'application/json',
|
|
'X-Subscription-Token': brave_api_key
|
|
}
|
|
|
|
# Search news
|
|
news_params = {
|
|
'q': query,
|
|
'count': 5,
|
|
'freshness': 'py', # past year
|
|
'country': 'pl',
|
|
'search_lang': 'pl'
|
|
}
|
|
|
|
news_response = requests.get(
|
|
'https://api.search.brave.com/res/v1/news/search',
|
|
headers=headers,
|
|
params=news_params,
|
|
timeout=10
|
|
)
|
|
|
|
if news_response.status_code == 200:
|
|
news_data = news_response.json()
|
|
for item in news_data.get('results', [])[:5]:
|
|
results['news'].append({
|
|
'title': item.get('title', ''),
|
|
'description': item.get('description', ''),
|
|
'url': item.get('url', ''),
|
|
'source': item.get('meta_url', {}).get('hostname', '')
|
|
})
|
|
logger.info(f"Brave News: found {len(results['news'])} items for '{company_name}'")
|
|
|
|
# Search web
|
|
web_params = {
|
|
'q': query,
|
|
'count': 5,
|
|
'country': 'pl',
|
|
'search_lang': 'pl'
|
|
}
|
|
|
|
web_response = requests.get(
|
|
'https://api.search.brave.com/res/v1/web/search',
|
|
headers=headers,
|
|
params=web_params,
|
|
timeout=10
|
|
)
|
|
|
|
if web_response.status_code == 200:
|
|
web_data = web_response.json()
|
|
for item in web_data.get('web', {}).get('results', [])[:5]:
|
|
results['web'].append({
|
|
'title': item.get('title', ''),
|
|
'description': item.get('description', ''),
|
|
'url': item.get('url', '')
|
|
})
|
|
logger.info(f"Brave Web: found {len(results['web'])} items for '{company_name}'")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Brave search error for '{company_name}': {e}")
|
|
|
|
return results
|
|
|
|
|
|
def _fetch_website_content(url: str) -> str:
|
|
"""
|
|
Fetch and extract text content from company website.
|
|
Returns first 3000 chars of text content.
|
|
"""
|
|
if not url:
|
|
return ''
|
|
|
|
try:
|
|
# Ensure URL has protocol
|
|
if not url.startswith('http'):
|
|
url = 'https://' + url
|
|
|
|
response = requests.get(url, timeout=10, headers={
|
|
'User-Agent': 'Mozilla/5.0 (compatible; NordaBizBot/1.0)'
|
|
})
|
|
|
|
if response.status_code == 200:
|
|
soup = BeautifulSoup(response.text, 'html.parser')
|
|
|
|
# Remove scripts and styles
|
|
for tag in soup(['script', 'style', 'nav', 'footer', 'header']):
|
|
tag.decompose()
|
|
|
|
# Get text content
|
|
text = soup.get_text(separator=' ', strip=True)
|
|
|
|
# Clean up whitespace
|
|
text = ' '.join(text.split())
|
|
|
|
logger.info(f"Fetched {len(text)} chars from {url}")
|
|
return text[:3000] # Limit to 3000 chars
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Failed to fetch website content from {url}: {e}")
|
|
|
|
return ''
|
|
|
|
|
|
# ============================================================
|
|
# AI ENRICHMENT API ROUTE
|
|
# ============================================================
|
|
|
|
@bp.route('/company/<int:company_id>/enrich-ai', methods=['POST'])
|
|
@login_required
|
|
def api_enrich_company_ai(company_id):
|
|
"""
|
|
API: Enrich company data using AI (Gemini) with web search.
|
|
|
|
Process:
|
|
1. Search Brave API for company news and web results
|
|
2. Fetch content from company website
|
|
3. Combine with existing database data
|
|
4. Send to Gemini for AI-powered enrichment
|
|
|
|
Generates AI insights including:
|
|
- Business summary
|
|
- Services list
|
|
- Target market
|
|
- Unique selling points
|
|
- Company values
|
|
- Certifications
|
|
- Industry tags
|
|
|
|
Requires: Admin or company owner permissions.
|
|
Rate limited to 5 requests per hour per user.
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
# Get company
|
|
company = db.query(Company).filter_by(id=company_id).first()
|
|
if not company:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Firma nie znaleziona'
|
|
}), 404
|
|
|
|
# Check permissions: user with company edit rights
|
|
logger.info(f"Permission check: user={current_user.email}, role={current_user.role}, user_company_id={current_user.company_id}, target_company_id={company.id}")
|
|
if not current_user.can_edit_company(company.id):
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Brak uprawnien. Tylko administrator lub wlasciciel firmy moze wzbogacac dane.'
|
|
}), 403
|
|
|
|
# Get Gemini service
|
|
service = gemini_service.get_gemini_service()
|
|
if not service:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Usluga AI jest niedostepna. Skontaktuj sie z administratorem.'
|
|
}), 503
|
|
|
|
logger.info(f"AI enrichment triggered by {current_user.email} for company: {company.name} (ID: {company.id})")
|
|
|
|
# ============================================
|
|
# STEP 1: Search the web for company info
|
|
# ============================================
|
|
brave_results = _search_brave_for_company(company.name, company.address_city)
|
|
|
|
# Format news for prompt
|
|
news_text = ""
|
|
if brave_results['news']:
|
|
news_text = "\n".join([
|
|
f"- {item['title']}: {item['description'][:200]}"
|
|
for item in brave_results['news'][:3]
|
|
])
|
|
|
|
# Format web results for prompt
|
|
web_text = ""
|
|
if brave_results['web']:
|
|
web_text = "\n".join([
|
|
f"- {item['title']}: {item['description'][:200]}"
|
|
for item in brave_results['web'][:3]
|
|
])
|
|
|
|
# ============================================
|
|
# STEP 2: Fetch company website content
|
|
# ============================================
|
|
website_content = ""
|
|
if company.website:
|
|
website_content = _fetch_website_content(company.website)
|
|
|
|
# ============================================
|
|
# STEP 3: Collect existing company data
|
|
# ============================================
|
|
services_list = []
|
|
if company.services:
|
|
services_list = [cs.service.name for cs in company.services if cs.service]
|
|
elif company.services_offered:
|
|
services_list = [company.services_offered]
|
|
|
|
competencies_list = []
|
|
if company.competencies:
|
|
competencies_list = [cc.competency.name for cc in company.competencies if cc.competency]
|
|
|
|
existing_data = {
|
|
'nazwa': company.name,
|
|
'opis_krotki': company.description_short or '',
|
|
'opis_pelny': company.description_full or '',
|
|
'kategoria': company.category.name if company.category else '',
|
|
'uslugi': ', '.join(services_list) if services_list else '',
|
|
'kompetencje': ', '.join(competencies_list) if competencies_list else '',
|
|
'wartosci': company.core_values or '',
|
|
'strona_www': company.website or '',
|
|
'miasto': company.address_city or '',
|
|
'branza': company.pkd_description or ''
|
|
}
|
|
|
|
# ============================================
|
|
# STEP 4: Build comprehensive prompt for AI
|
|
# ============================================
|
|
prompt = f"""Przeanalizuj wszystkie dostepne dane o polskiej firmie i wygeneruj wzbogacone informacje.
|
|
|
|
=== DANE Z BAZY DANYCH ===
|
|
Nazwa: {existing_data['nazwa']}
|
|
Kategoria: {existing_data['kategoria']}
|
|
Opis krotki: {existing_data['opis_krotki']}
|
|
Opis pelny: {existing_data['opis_pelny']}
|
|
Uslugi: {existing_data['uslugi']}
|
|
Kompetencje: {existing_data['kompetencje']}
|
|
Wartosci firmy: {existing_data['wartosci']}
|
|
Strona WWW: {existing_data['strona_www']}
|
|
Miasto: {existing_data['miasto']}
|
|
Branza (PKD): {existing_data['branza']}
|
|
|
|
=== INFORMACJE Z INTERNETU (Brave Search) ===
|
|
Newsy o firmie:
|
|
{news_text if news_text else '(brak znalezionych newsow)'}
|
|
|
|
Wyniki wyszukiwania:
|
|
{web_text if web_text else '(brak wynikow)'}
|
|
|
|
=== TRESC ZE STRONY WWW FIRMY ===
|
|
{website_content[:2000] if website_content else '(nie udalo sie pobrac tresci strony)'}
|
|
|
|
=== ZADANIE ===
|
|
Na podstawie WSZYSTKICH powyzszych danych (baza danych, wyszukiwarka, strona WWW) wygeneruj wzbogacone informacje o firmie.
|
|
Wykorzystaj informacje z internetu do uzupelnienia brakujacych danych.
|
|
Jesli znalazles nowe uslugi, certyfikaty lub informacje - dodaj je do odpowiedzi.
|
|
|
|
Odpowiedz WYLACZNIE w formacie JSON (bez dodatkowego tekstu):
|
|
{{
|
|
"business_summary": "Zwiezly opis dzialalnosci firmy (2-3 zdania) na podstawie wszystkich zrodel",
|
|
"services_list": ["usluga1", "usluga2", "usluga3", "usluga4", "usluga5"],
|
|
"target_market": "Opis grupy docelowej klientow",
|
|
"unique_selling_points": ["wyroznik1", "wyroznik2", "wyroznik3"],
|
|
"company_values": ["wartosc1", "wartosc2", "wartosc3"],
|
|
"certifications": ["certyfikat1", "certyfikat2"],
|
|
"industry_tags": ["tag1", "tag2", "tag3", "tag4", "tag5"],
|
|
"recent_news": "Krotkie podsumowanie ostatnich newsow o firmie (jesli sa)",
|
|
"suggested_category": "Sugerowana kategoria glowna",
|
|
"category_confidence": 0.85,
|
|
"data_sources_used": ["database", "brave_search", "website"]
|
|
}}
|
|
|
|
WAZNE:
|
|
- Odpowiedz TYLKO JSON, bez markdown, bez ```json
|
|
- Wszystkie teksty po polsku
|
|
- Listy powinny zawierac 3-5 elementow
|
|
- category_confidence to liczba od 0 do 1
|
|
- Wykorzystaj maksymalnie informacje z internetu
|
|
"""
|
|
|
|
# Call Gemini API
|
|
start_time = time.time()
|
|
response_text = service.generate_text(
|
|
prompt=prompt,
|
|
temperature=0.7,
|
|
feature='ai_enrichment',
|
|
user_id=current_user.id,
|
|
company_id=company.id,
|
|
related_entity_type='company',
|
|
related_entity_id=company.id
|
|
)
|
|
processing_time = int((time.time() - start_time) * 1000)
|
|
|
|
# Parse JSON response
|
|
try:
|
|
# Clean response - remove markdown code blocks if present
|
|
clean_response = response_text.strip()
|
|
if clean_response.startswith('```'):
|
|
clean_response = clean_response.split('```')[1]
|
|
if clean_response.startswith('json'):
|
|
clean_response = clean_response[4:]
|
|
clean_response = clean_response.strip()
|
|
|
|
ai_data = json.loads(clean_response)
|
|
except json.JSONDecodeError as e:
|
|
logger.error(f"Failed to parse AI response: {e}\nResponse: {response_text[:500]}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Blad parsowania odpowiedzi AI. Sprobuj ponownie.'
|
|
}), 500
|
|
|
|
# Create AI enrichment PROPOSAL (requires approval before applying)
|
|
# Instead of directly saving, we create a proposal that needs to be reviewed
|
|
|
|
# Count sources used (needs to be before creating proposal)
|
|
sources_used = ['database']
|
|
if brave_results['news'] or brave_results['web']:
|
|
sources_used.append('brave_search')
|
|
if website_content:
|
|
sources_used.append('website')
|
|
|
|
# Check for existing pending proposals
|
|
existing_pending = db.query(AiEnrichmentProposal).filter_by(
|
|
company_id=company.id,
|
|
status='pending'
|
|
).first()
|
|
|
|
if existing_pending:
|
|
# Update existing pending proposal
|
|
existing_pending.proposed_data = ai_data
|
|
existing_pending.data_source = company.website
|
|
existing_pending.confidence_score = 0.85
|
|
existing_pending.ai_explanation = f"AI przeanalizowało dane z {len(sources_used)} źródeł: {', '.join(sources_used)}"
|
|
existing_pending.created_at = datetime.utcnow()
|
|
existing_pending.expires_at = datetime.utcnow() + timedelta(days=30)
|
|
proposal = existing_pending
|
|
else:
|
|
# Create new proposal
|
|
proposal = AiEnrichmentProposal(
|
|
company_id=company.id,
|
|
status='pending',
|
|
proposal_type='ai_enrichment',
|
|
data_source=company.website,
|
|
proposed_data=ai_data,
|
|
ai_explanation=f"AI przeanalizowało dane z {len(sources_used)} źródeł: {', '.join(sources_used)}",
|
|
confidence_score=0.85,
|
|
expires_at=datetime.utcnow() + timedelta(days=30)
|
|
)
|
|
db.add(proposal)
|
|
|
|
db.commit()
|
|
proposal_id = proposal.id
|
|
|
|
logger.info(f"AI enrichment proposal created for {company.name}. Proposal ID: {proposal_id}. Sources: {sources_used}")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': f'Propozycja wzbogacenia danych dla "{company.name}" została utworzona i oczekuje na akceptację',
|
|
'proposal_id': proposal_id,
|
|
'status': 'pending',
|
|
'processing_time_ms': processing_time,
|
|
'sources_used': sources_used,
|
|
'brave_results_count': len(brave_results['news']) + len(brave_results['web']),
|
|
'website_content_length': len(website_content),
|
|
'proposed_data': ai_data,
|
|
'requires_approval': True
|
|
})
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"AI enrichment error for company {company_id}: {str(e)}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Blad podczas wzbogacania danych: {str(e)}'
|
|
}), 500
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# ============================================================
|
|
# AI ENRICHMENT PROPOSALS API ROUTES
|
|
# ============================================================
|
|
|
|
@bp.route('/company/<int:company_id>/proposals', methods=['GET'])
|
|
@login_required
|
|
def api_get_proposals(company_id):
|
|
"""
|
|
API: Get AI enrichment proposals for a company.
|
|
|
|
Returns pending, approved, and rejected proposals.
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
company = db.query(Company).filter_by(id=company_id).first()
|
|
if not company:
|
|
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
|
|
|
|
# Check permissions - user with company edit rights
|
|
if not current_user.can_edit_company(company.id):
|
|
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
|
|
|
|
proposals = db.query(AiEnrichmentProposal).filter_by(
|
|
company_id=company_id
|
|
).order_by(AiEnrichmentProposal.created_at.desc()).all()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'proposals': [{
|
|
'id': p.id,
|
|
'status': p.status,
|
|
'proposal_type': p.proposal_type,
|
|
'proposed_data': p.proposed_data,
|
|
'ai_explanation': p.ai_explanation,
|
|
'confidence_score': float(p.confidence_score) if p.confidence_score else None,
|
|
'created_at': p.created_at.isoformat() if p.created_at else None,
|
|
'reviewed_at': p.reviewed_at.isoformat() if p.reviewed_at else None,
|
|
'reviewed_by': p.reviewed_by.email if p.reviewed_by else None,
|
|
'review_comment': p.review_comment,
|
|
'approved_fields': p.approved_fields
|
|
} for p in proposals]
|
|
})
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/company/<int:company_id>/proposals/<int:proposal_id>/approve', methods=['POST'])
|
|
@login_required
|
|
def api_approve_proposal(company_id, proposal_id):
|
|
"""
|
|
API: Approve an AI enrichment proposal.
|
|
|
|
Optionally accepts 'fields' parameter to approve only specific fields.
|
|
When approved, the data is applied to CompanyAIInsights.
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
company = db.query(Company).filter_by(id=company_id).first()
|
|
if not company:
|
|
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
|
|
|
|
# Check permissions - user with company edit rights
|
|
if not current_user.can_edit_company(company.id):
|
|
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
|
|
|
|
proposal = db.query(AiEnrichmentProposal).filter_by(
|
|
id=proposal_id,
|
|
company_id=company_id
|
|
).first()
|
|
|
|
if not proposal:
|
|
return jsonify({'success': False, 'error': 'Propozycja nie istnieje'}), 404
|
|
|
|
if proposal.status != 'pending':
|
|
return jsonify({'success': False, 'error': f'Propozycja ma status: {proposal.status}'}), 400
|
|
|
|
data = request.get_json() or {}
|
|
approved_fields = data.get('fields') # Optional: only approve specific fields
|
|
comment = data.get('comment', '')
|
|
|
|
ai_data = proposal.proposed_data
|
|
|
|
# Apply to CompanyAIInsights
|
|
existing_insights = db.query(CompanyAIInsights).filter_by(company_id=company.id).first()
|
|
|
|
# Determine which fields to apply
|
|
if approved_fields:
|
|
# Partial approval
|
|
fields_to_apply = approved_fields
|
|
else:
|
|
# Full approval - all fields
|
|
fields_to_apply = list(ai_data.keys())
|
|
|
|
if existing_insights:
|
|
# Update existing
|
|
if 'business_summary' in fields_to_apply:
|
|
existing_insights.business_summary = ai_data.get('business_summary')
|
|
if 'services_list' in fields_to_apply:
|
|
existing_insights.services_list = ai_data.get('services_list', [])
|
|
if 'target_market' in fields_to_apply:
|
|
existing_insights.target_market = ai_data.get('target_market')
|
|
if 'unique_selling_points' in fields_to_apply:
|
|
existing_insights.unique_selling_points = ai_data.get('unique_selling_points', [])
|
|
if 'company_values' in fields_to_apply:
|
|
existing_insights.company_values = ai_data.get('company_values', [])
|
|
if 'certifications' in fields_to_apply:
|
|
existing_insights.certifications = ai_data.get('certifications', [])
|
|
if 'industry_tags' in fields_to_apply:
|
|
existing_insights.industry_tags = ai_data.get('industry_tags', [])
|
|
if 'suggested_category' in fields_to_apply:
|
|
existing_insights.suggested_category = ai_data.get('suggested_category')
|
|
existing_insights.ai_confidence_score = proposal.confidence_score
|
|
existing_insights.analyzed_at = datetime.utcnow()
|
|
else:
|
|
# Create new
|
|
new_insights = CompanyAIInsights(
|
|
company_id=company.id,
|
|
business_summary=ai_data.get('business_summary') if 'business_summary' in fields_to_apply else None,
|
|
services_list=ai_data.get('services_list', []) if 'services_list' in fields_to_apply else [],
|
|
target_market=ai_data.get('target_market') if 'target_market' in fields_to_apply else None,
|
|
unique_selling_points=ai_data.get('unique_selling_points', []) if 'unique_selling_points' in fields_to_apply else [],
|
|
company_values=ai_data.get('company_values', []) if 'company_values' in fields_to_apply else [],
|
|
certifications=ai_data.get('certifications', []) if 'certifications' in fields_to_apply else [],
|
|
industry_tags=ai_data.get('industry_tags', []) if 'industry_tags' in fields_to_apply else [],
|
|
suggested_category=ai_data.get('suggested_category') if 'suggested_category' in fields_to_apply else None,
|
|
ai_confidence_score=proposal.confidence_score,
|
|
analyzed_at=datetime.utcnow()
|
|
)
|
|
db.add(new_insights)
|
|
|
|
# Update proposal status
|
|
proposal.status = 'approved'
|
|
proposal.reviewed_at = datetime.utcnow()
|
|
proposal.reviewed_by_id = current_user.id
|
|
proposal.review_comment = comment
|
|
proposal.approved_fields = fields_to_apply
|
|
proposal.applied_at = datetime.utcnow()
|
|
|
|
db.commit()
|
|
|
|
logger.info(f"AI proposal {proposal_id} approved for company {company.name} by {current_user.email}")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': f'Propozycja została zaakceptowana i dane zastosowane do profilu',
|
|
'approved_fields': fields_to_apply
|
|
})
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Error approving proposal {proposal_id}: {str(e)}")
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/company/<int:company_id>/proposals/<int:proposal_id>/reject', methods=['POST'])
|
|
@login_required
|
|
def api_reject_proposal(company_id, proposal_id):
|
|
"""
|
|
API: Reject an AI enrichment proposal.
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
company = db.query(Company).filter_by(id=company_id).first()
|
|
if not company:
|
|
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
|
|
|
|
# Check permissions - user with company edit rights
|
|
if not current_user.can_edit_company(company.id):
|
|
return jsonify({'success': False, 'error': 'Brak uprawnien'}), 403
|
|
|
|
proposal = db.query(AiEnrichmentProposal).filter_by(
|
|
id=proposal_id,
|
|
company_id=company_id
|
|
).first()
|
|
|
|
if not proposal:
|
|
return jsonify({'success': False, 'error': 'Propozycja nie istnieje'}), 404
|
|
|
|
if proposal.status != 'pending':
|
|
return jsonify({'success': False, 'error': f'Propozycja ma status: {proposal.status}'}), 400
|
|
|
|
data = request.get_json() or {}
|
|
comment = data.get('comment', '')
|
|
|
|
# Update proposal status
|
|
proposal.status = 'rejected'
|
|
proposal.reviewed_at = datetime.utcnow()
|
|
proposal.reviewed_by_id = current_user.id
|
|
proposal.review_comment = comment
|
|
|
|
db.commit()
|
|
|
|
logger.info(f"AI proposal {proposal_id} rejected for company {company.name} by {current_user.email}")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Propozycja została odrzucona'
|
|
})
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Error rejecting proposal {proposal_id}: {str(e)}")
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# ============================================================
|
|
# UTILITY API ROUTES
|
|
# ============================================================
|
|
|
|
@bp.route('/model-info', methods=['GET'])
|
|
def api_model_info():
|
|
"""API: Get current AI model information"""
|
|
service = gemini_service.get_gemini_service()
|
|
if service:
|
|
return jsonify({
|
|
'success': True,
|
|
'model': service.model_name,
|
|
'provider': 'Google Gemini'
|
|
})
|
|
else:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'AI service not initialized'
|
|
}), 500
|
|
|
|
|
|
@bp.route('/admin/test-sanitization', methods=['POST'])
|
|
@login_required
|
|
def test_sanitization():
|
|
"""
|
|
Admin API: Test sensitive data detection without saving.
|
|
Allows admins to verify what data would be sanitized.
|
|
"""
|
|
if not current_user.can_access_admin_panel():
|
|
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
|
|
|
try:
|
|
from sensitive_data_service import sanitize_message
|
|
data = request.get_json()
|
|
text = data.get('text', '')
|
|
|
|
if not text:
|
|
return jsonify({'success': False, 'error': 'Text is required'}), 400
|
|
|
|
sanitized, matches = sanitize_message(text)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'original': text,
|
|
'sanitized': sanitized,
|
|
'matches': [
|
|
{
|
|
'type': m.data_type.value,
|
|
'original': m.original,
|
|
'masked': m.masked,
|
|
'confidence': m.confidence
|
|
}
|
|
for m in matches
|
|
],
|
|
'has_sensitive_data': len(matches) > 0
|
|
})
|
|
except ImportError:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Sensitive data service not available'
|
|
}), 500
|
|
except Exception as e:
|
|
logger.error(f"Error testing sanitization: {e}")
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|