- Created blueprints/api/routes_contacts.py with 2 routes: - /api/contacts/ai-parse (POST) - /api/contacts/bulk-create (POST) - Includes AI prompts for contact parsing - Removed ~300 lines from app.py (7063 -> 6764) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
321 lines
12 KiB
Python
321 lines
12 KiB
Python
"""
|
|
Contacts API Routes - API blueprint
|
|
|
|
Migrated from app.py as part of the blueprint refactoring.
|
|
Contains API routes for AI-assisted external contact creation.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import tempfile
|
|
|
|
from flask import jsonify, request
|
|
from flask_login import current_user, login_required
|
|
|
|
from database import SessionLocal, ExternalContact
|
|
import gemini_service
|
|
from . import bp
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================
|
|
# AI PROMPTS FOR CONTACT PARSING
|
|
# ============================================================
|
|
|
|
AI_CONTACT_PARSE_PROMPT = """Jesteś asystentem systemu NordaBiz pomagającym dodawać kontakty zewnętrzne.
|
|
|
|
ZADANIE:
|
|
Przeanalizuj podany tekst i wyodrębnij informacje o osobach kontaktowych z zewnętrznych organizacji
|
|
(urzędy, agencje, instytucje, firmy partnerskie - osoby spoza Norda Biznes).
|
|
|
|
DANE WEJŚCIOWE:
|
|
```
|
|
{input_text}
|
|
```
|
|
|
|
TYPY ORGANIZACJI:
|
|
- government = Urząd (np. ministerstwo, urząd gminy/powiatu)
|
|
- agency = Agencja (np. ARP, PARP, agencje rozwoju)
|
|
- company = Firma (przedsiębiorstwa, spółki)
|
|
- ngo = Organizacja pozarządowa (fundacje, stowarzyszenia)
|
|
- university = Uczelnia (uniwersytety, politechniki)
|
|
- other = Inne
|
|
|
|
INSTRUKCJE:
|
|
1. Wyodrębnij każdą osobę kontaktową z tekstu
|
|
2. Dla każdej osoby zidentyfikuj:
|
|
- imię i nazwisko (WYMAGANE)
|
|
- stanowisko/funkcja (jeśli dostępne)
|
|
- telefon (jeśli dostępny)
|
|
- email (jeśli dostępny)
|
|
- organizacja (WYMAGANE - nazwa instytucji)
|
|
- typ organizacji (government/agency/company/ngo/university/other)
|
|
- projekt/kontekst (jeśli tekst wspomina o konkretnym projekcie)
|
|
- tagi (słowa kluczowe związane z osobą/projektem)
|
|
3. Jeśli brak imienia i nazwiska - pomiń osobę
|
|
4. Jeśli brak nazwy organizacji - pomiń osobę
|
|
|
|
ZWRÓĆ TYLKO CZYSTY JSON w dokładnie takim formacie (bez żadnego tekstu przed ani po):
|
|
{{
|
|
"analysis": "Krótki opis znalezionych kontaktów (1-2 zdania po polsku)",
|
|
"contacts": [
|
|
{{
|
|
"first_name": "Imię",
|
|
"last_name": "Nazwisko",
|
|
"position": "Stanowisko lub null",
|
|
"phone": "Numer telefonu lub null",
|
|
"email": "Email lub null",
|
|
"organization_name": "Nazwa organizacji",
|
|
"organization_type": "government|agency|company|ngo|university|other",
|
|
"project_name": "Nazwa projektu lub null",
|
|
"tags": "tagi, oddzielone, przecinkami",
|
|
"warnings": []
|
|
}}
|
|
]
|
|
}}"""
|
|
|
|
AI_CONTACT_IMAGE_PROMPT = """Jesteś asystentem systemu NordaBiz pomagającym dodawać kontakty zewnętrzne.
|
|
|
|
ZADANIE:
|
|
Przeanalizuj ten obraz (screenshot) i wyodrębnij informacje o osobach kontaktowych.
|
|
Szukaj: imion i nazwisk, stanowisk, telefonów, emaili, nazw organizacji, projektów.
|
|
|
|
TYPY ORGANIZACJI:
|
|
- government = Urząd (np. ministerstwo, urząd gminy/powiatu)
|
|
- agency = Agencja (np. ARP, PARP, agencje rozwoju)
|
|
- company = Firma (przedsiębiorstwa, spółki)
|
|
- ngo = Organizacja pozarządowa (fundacje, stowarzyszenia)
|
|
- university = Uczelnia (uniwersytety, politechniki)
|
|
- other = Inne
|
|
|
|
INSTRUKCJE:
|
|
1. Przeczytaj cały tekst widoczny na obrazie
|
|
2. Wyodrębnij każdą osobę kontaktową
|
|
3. Dla każdej osoby zidentyfikuj:
|
|
- imię i nazwisko (WYMAGANE)
|
|
- stanowisko/funkcja
|
|
- telefon
|
|
- email
|
|
- organizacja (WYMAGANE)
|
|
- typ organizacji
|
|
- projekt/kontekst
|
|
- tagi
|
|
4. Jeśli brak imienia/nazwiska lub organizacji - pomiń osobę
|
|
|
|
ZWRÓĆ TYLKO CZYSTY JSON w dokładnie takim formacie:
|
|
{{
|
|
"analysis": "Krótki opis znalezionych kontaktów (1-2 zdania po polsku)",
|
|
"contacts": [
|
|
{{
|
|
"first_name": "Imię",
|
|
"last_name": "Nazwisko",
|
|
"position": "Stanowisko lub null",
|
|
"phone": "Numer telefonu lub null",
|
|
"email": "Email lub null",
|
|
"organization_name": "Nazwa organizacji",
|
|
"organization_type": "government|agency|company|ngo|university|other",
|
|
"project_name": "Nazwa projektu lub null",
|
|
"tags": "tagi, oddzielone, przecinkami",
|
|
"warnings": []
|
|
}}
|
|
]
|
|
}}"""
|
|
|
|
|
|
# ============================================================
|
|
# CONTACTS API ROUTES
|
|
# ============================================================
|
|
|
|
@bp.route('/contacts/ai-parse', methods=['POST'])
|
|
@login_required
|
|
def contacts_ai_parse():
|
|
"""Parse text or image with AI to extract external contact data."""
|
|
db = SessionLocal()
|
|
try:
|
|
# Check input type
|
|
input_type = request.form.get('input_type') or (request.get_json() or {}).get('input_type', 'text')
|
|
|
|
if input_type == 'image':
|
|
# Handle image upload
|
|
if 'file' not in request.files:
|
|
return jsonify({'success': False, 'error': 'Brak pliku obrazu'}), 400
|
|
|
|
file = request.files['file']
|
|
if file.filename == '':
|
|
return jsonify({'success': False, 'error': 'Nie wybrano pliku'}), 400
|
|
|
|
# Validate file type
|
|
allowed_extensions = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
|
ext = file.filename.rsplit('.', 1)[-1].lower() if '.' in file.filename else ''
|
|
if ext not in allowed_extensions:
|
|
return jsonify({'success': False, 'error': 'Dozwolone formaty: PNG, JPG, JPEG, GIF, WEBP'}), 400
|
|
|
|
# Save temp file
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix=f'.{ext}') as tmp:
|
|
file.save(tmp.name)
|
|
temp_path = tmp.name
|
|
|
|
try:
|
|
# Get Gemini service and analyze image
|
|
service = gemini_service.get_gemini_service()
|
|
ai_response = service.analyze_image(temp_path, AI_CONTACT_IMAGE_PROMPT)
|
|
finally:
|
|
# Clean up temp file
|
|
if os.path.exists(temp_path):
|
|
os.unlink(temp_path)
|
|
|
|
else:
|
|
# Handle text input
|
|
data = request.get_json() or {}
|
|
# Support both 'text' (from frontend modal) and 'content' for backwards compatibility
|
|
content = (data.get('text') or data.get('content', '')).strip()
|
|
|
|
if not content:
|
|
return jsonify({'success': False, 'error': 'Brak treści do analizy'}), 400
|
|
|
|
# Get Gemini service and analyze text
|
|
service = gemini_service.get_gemini_service()
|
|
prompt = AI_CONTACT_PARSE_PROMPT.format(input_text=content)
|
|
ai_response = service.generate_text(
|
|
prompt=prompt,
|
|
feature='ai_contact_parse',
|
|
user_id=current_user.id,
|
|
temperature=0.3
|
|
)
|
|
|
|
# Parse AI response as JSON
|
|
json_match = re.search(r'\{[\s\S]*\}', ai_response)
|
|
if not json_match:
|
|
logger.error(f"AI contact response not valid JSON: {ai_response[:500]}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'AI nie zwróciło prawidłowej odpowiedzi. Spróbuj ponownie.'
|
|
}), 500
|
|
|
|
try:
|
|
parsed = json.loads(json_match.group())
|
|
except json.JSONDecodeError as e:
|
|
logger.error(f"JSON parse error: {e}, response: {ai_response[:500]}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Błąd parsowania odpowiedzi AI. Spróbuj ponownie.'
|
|
}), 500
|
|
|
|
# Check for potential duplicates
|
|
proposed_contacts = parsed.get('contacts', [])
|
|
|
|
for contact in proposed_contacts:
|
|
first_name = contact.get('first_name', '').strip()
|
|
last_name = contact.get('last_name', '').strip()
|
|
org_name = contact.get('organization_name', '').strip()
|
|
|
|
if first_name and last_name and org_name:
|
|
# Check for existing similar contact
|
|
existing = db.query(ExternalContact).filter(
|
|
ExternalContact.first_name.ilike(first_name),
|
|
ExternalContact.last_name.ilike(last_name),
|
|
ExternalContact.organization_name.ilike(f'%{org_name}%'),
|
|
ExternalContact.is_active == True
|
|
).first()
|
|
|
|
if existing:
|
|
contact['warnings'] = contact.get('warnings', []) + [
|
|
f'Podobny kontakt może już istnieć: {existing.full_name} @ {existing.organization_name}'
|
|
]
|
|
contact['potential_duplicate_id'] = existing.id
|
|
|
|
logger.info(f"User {current_user.email} used AI to parse contacts: {len(proposed_contacts)} found")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'analysis': parsed.get('analysis', 'Analiza zakończona'),
|
|
'contacts': proposed_contacts
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in AI contact parse: {e}")
|
|
return jsonify({'success': False, 'error': f'Błąd: {str(e)}'}), 500
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/contacts/bulk-create', methods=['POST'])
|
|
@login_required
|
|
def contacts_bulk_create():
|
|
"""Create multiple external contacts from confirmed proposals."""
|
|
db = SessionLocal()
|
|
try:
|
|
data = request.get_json() or {}
|
|
contacts_to_create = data.get('contacts', [])
|
|
|
|
if not contacts_to_create:
|
|
return jsonify({'success': False, 'error': 'Brak kontaktów do utworzenia'}), 400
|
|
|
|
created = []
|
|
failed = []
|
|
|
|
for contact_data in contacts_to_create:
|
|
try:
|
|
# Validate required fields
|
|
first_name = contact_data.get('first_name', '').strip()
|
|
last_name = contact_data.get('last_name', '').strip()
|
|
organization_name = contact_data.get('organization_name', '').strip()
|
|
|
|
if not first_name or not last_name or not organization_name:
|
|
failed.append({
|
|
'name': f"{first_name} {last_name}",
|
|
'error': 'Brak wymaganych danych (imię, nazwisko lub organizacja)'
|
|
})
|
|
continue
|
|
|
|
# Create contact
|
|
contact = ExternalContact(
|
|
first_name=first_name,
|
|
last_name=last_name,
|
|
position=contact_data.get('position', '').strip() or None,
|
|
phone=contact_data.get('phone', '').strip() or None,
|
|
email=contact_data.get('email', '').strip() or None,
|
|
organization_name=organization_name,
|
|
organization_type=contact_data.get('organization_type', 'other'),
|
|
project_name=contact_data.get('project_name', '').strip() or None,
|
|
tags=contact_data.get('tags', '').strip() or None,
|
|
source_type='ai_import',
|
|
created_by=current_user.id
|
|
)
|
|
|
|
db.add(contact)
|
|
db.flush()
|
|
|
|
created.append({
|
|
'id': contact.id,
|
|
'name': contact.full_name,
|
|
'organization': contact.organization_name
|
|
})
|
|
|
|
except Exception as e:
|
|
failed.append({
|
|
'name': f"{contact_data.get('first_name', '')} {contact_data.get('last_name', '')}",
|
|
'error': str(e)
|
|
})
|
|
|
|
db.commit()
|
|
|
|
logger.info(f"User {current_user.email} bulk created {len(created)} contacts via AI")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'created': created,
|
|
'failed': failed,
|
|
'message': f'Utworzono {len(created)} kontaktów' + (f', {len(failed)} błędów' if failed else '')
|
|
})
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Error in contacts bulk create: {e}")
|
|
return jsonify({'success': False, 'error': f'Błąd: {str(e)}'}), 500
|
|
finally:
|
|
db.close()
|