Merge pull request 'neue-Kennzahl-spacy' (#94) from neue-Kennzahl-spacy into main

Reviewed-on: #94
main
Anastasia Hanna Ougolnikova 2025-06-29 18:23:42 +02:00
commit 5d0a5ab3c3
56 changed files with 12897 additions and 1439 deletions

View File

@ -26,6 +26,6 @@ def health_check():
return "OK"
# für Docker wichtig: host='0.0.0.0'
# Für Docker wichtig: host='0.0.0.0'
if __name__ == "__main__":
socketio.run(app, debug=True, host="0.0.0.0", port=5050)

View File

@ -1,11 +1,12 @@
from controller.spacy_contoller import spacy_controller
from controller.kpi_setting_controller import kpi_setting_controller
from controller.spacy_controller import spacy_controller
from controller.kpi_setting_controller import kpi_setting_controller, kpi_routes
from controller.pitch_book_controller import pitch_book_controller
from controller.progress_controller import progress_controller
def register_routes(app):
app.register_blueprint(kpi_setting_controller)
app.register_blueprint(kpi_routes)
app.register_blueprint(pitch_book_controller)
app.register_blueprint(spacy_controller)
app.register_blueprint(progress_controller)

View File

@ -2,7 +2,10 @@ from flask import Blueprint, request, jsonify
from model.database import db
from model.kpi_setting_model import KPISettingModel, KPISettingType
# Routen für /api/kpi/settings (Auslesen im Frontend)
kpi_routes = Blueprint("kpi_routes", __name__, url_prefix="/api/kpi")
# Routen für /api/kpi_setting/ (Hinzufügen, Ändern, Löschen)
kpi_setting_controller = Blueprint(
"kpi_settings", __name__, url_prefix="/api/kpi_setting"
)
@ -14,12 +17,6 @@ def get_all_kpi_settings():
return jsonify([kpi_setting.to_dict() for kpi_setting in kpi_settings]), 200
@kpi_setting_controller.route("/<int:id>", methods=["GET"])
def get_kpi_setting(id):
kpi_setting = KPISettingModel.query.get_or_404(id)
return jsonify(kpi_setting.to_dict()), 200
@kpi_setting_controller.route("/", methods=["POST"])
def create_kpi_setting():
data = request.json
@ -29,13 +26,12 @@ def create_kpi_setting():
required_fields = [
"name",
"description",
"mandatory",
"type",
"translation",
"example",
"position",
"active",
"examples",
"is_trained",
]
for field in required_fields:
if field not in data:
@ -55,13 +51,12 @@ def create_kpi_setting():
new_kpi_setting = KPISettingModel(
name=data["name"],
description=data["description"],
mandatory=data["mandatory"],
type=kpi_type,
translation=data["translation"],
example=data["example"],
position=data["position"],
active=data["active"],
examples=data.get("examples", []),
is_trained=False,
)
db.session.add(new_kpi_setting)
@ -84,9 +79,6 @@ def update_kpi_setting(id):
return jsonify({"error": "KPI Setting with this name already exists"}), 409
kpi_setting.name = data["name"]
if "description" in data:
kpi_setting.description = data["description"]
if "mandatory" in data:
kpi_setting.mandatory = data["mandatory"]
@ -100,18 +92,18 @@ def update_kpi_setting(id):
400,
)
if "translation" in data:
kpi_setting.translation = data["translation"]
if "example" in data:
kpi_setting.example = data["example"]
if "position" in data:
kpi_setting.position = data["position"]
if "active" in data:
kpi_setting.active = data["active"]
if "examples" in data:
kpi_setting.examples = data["examples"]
if "is_trained" in data:
kpi_setting.is_trained = data["is_trained"]
db.session.commit()
return jsonify(kpi_setting.to_dict()), 200
@ -154,3 +146,21 @@ def update_kpi_positions():
except Exception as e:
db.session.rollback()
return jsonify({"error": f"Failed to update positions: {str(e)}"}), 500
@kpi_routes.route("/settings", methods=["GET"])
def get_kpi_settings():
try:
kpis = KPISettingModel.query.all()
return jsonify([k.to_dict() for k in kpis]), 200
except Exception as e:
return (
jsonify({"error": "Fehler beim Abrufen der KPIs", "details": str(e)}),
500,
)
@kpi_setting_controller.route("/<int:id>", methods=["GET"])
def get_kpi_setting(id):
kpi_setting = KPISettingModel.query.get_or_404(id)
return jsonify(kpi_setting.to_dict()), 200

View File

@ -1,93 +0,0 @@
from flask import Blueprint, request, jsonify, send_file
from io import BytesIO
from model.spacy_model import SpacyModel
import puremagic
from werkzeug.utils import secure_filename
from model.database import db
spacy_controller = Blueprint("spacy", __name__, url_prefix="/api/spacy")
@spacy_controller.route("/", methods=["GET"])
def get_all_files():
files = SpacyModel.query.all()
return jsonify([file.to_dict() for file in files]), 200
@spacy_controller.route("/<int:id>", methods=["GET"])
def get_file(id):
file = SpacyModel.query.get_or_404(id)
return jsonify(file.to_dict()), 200
@spacy_controller.route("/<int:id>/download", methods=["GET"])
def download_file(id):
file = SpacyModel.query.get_or_404(id)
return send_file(
BytesIO(file.file), download_name=file.filename, as_attachment=True
)
@spacy_controller.route("/", methods=["POST"])
def upload_file():
print(request)
if "file" not in request.files:
return jsonify({"error": "No file part in the request"}), 400
uploaded_file = request.files["file"]
if uploaded_file.filename == "":
return jsonify({"error": "No selected file"}), 400
# Read file data once
file_data = uploaded_file.read()
try:
if uploaded_file:
fileName = uploaded_file.filename or ""
new_file = SpacyModel(filename=secure_filename(fileName), file=file_data)
db.session.add(new_file)
db.session.commit()
return jsonify(new_file.to_dict()), 201
except Exception as e:
print(e)
return jsonify({"error": "Invalid file format. Only PDF files are accepted"}), 400
@spacy_controller.route("/<int:id>", methods=["PUT"])
def update_file(id):
file = SpacyModel.query.get_or_404(id)
if "file" in request.files:
uploaded_file = request.files["file"]
if uploaded_file.filename != "":
file.filename = uploaded_file.filename
# Read file data once
file_data = uploaded_file.read()
try:
if (
uploaded_file
and puremagic.from_string(file_data, mime=True) == "application/pdf"
):
file.file = file_data
except Exception as e:
print(e)
if "kpi" in request.form:
file.kpi = request.form.get("kpi")
db.session.commit()
return jsonify(file.to_dict()), 200
@spacy_controller.route("/<int:id>", methods=["DELETE"])
def delete_file(id):
file = SpacyModel.query.get_or_404(id)
db.session.delete(file)
db.session.commit()
return jsonify({"message": f"File {id} deleted successfully"}), 200

View File

@ -0,0 +1,149 @@
from flask import Blueprint, request, jsonify, send_file
from io import BytesIO
from model.spacy_model import SpacyModel
import puremagic
from werkzeug.utils import secure_filename
from model.database import db
import os
import requests
from model.kpi_setting_model import KPISettingModel
spacy_controller = Blueprint("spacy", __name__, url_prefix="/api/spacy")
SPACY_TRAINING_URL = os.getenv("SPACY_TRAINING_URL", "http://spacy:5052/train")
SPACY_URL = os.getenv("SPACY_URL", "http://spacy:5052")
@spacy_controller.route("/train", methods=["POST"])
def trigger_training():
try:
response = requests.post(SPACY_TRAINING_URL, timeout=600)
if response.ok:
return jsonify({"message": "Training erfolgreich angestoßen."}), 200
else:
return (
jsonify({"error": "Training fehlgeschlagen", "details": response.text}),
500,
)
except Exception as e:
return (
jsonify(
{"error": "Fehler beim Senden an Trainingsservice", "details": str(e)}
),
500,
)
@spacy_controller.route("/", methods=["GET"])
def get_all_files():
files = SpacyModel.query.all()
return jsonify([file.to_dict() for file in files]), 200
@spacy_controller.route("/<int:id>", methods=["GET"])
def get_file(id):
file = SpacyModel.query.get_or_404(id)
return jsonify(file.to_dict()), 200
@spacy_controller.route("/<int:id>/download", methods=["GET"])
def download_file(id):
file = SpacyModel.query.get_or_404(id)
return send_file(
BytesIO(file.file), download_name=file.filename, as_attachment=True
)
@spacy_controller.route("/", methods=["POST"])
def upload_file():
if "file" not in request.files:
return jsonify({"error": "No file part in the request"}), 400
uploaded_file = request.files["file"]
if uploaded_file.filename == "":
return jsonify({"error": "No selected file"}), 400
file_data = uploaded_file.read()
try:
fileName = uploaded_file.filename or ""
new_file = SpacyModel(filename=secure_filename(fileName), file=file_data)
db.session.add(new_file)
db.session.commit()
return jsonify(new_file.to_dict()), 201
except Exception as e:
print(e)
return jsonify({"error": "Invalid file format. Only PDF files are accepted"}), 400
@spacy_controller.route("/<int:id>", methods=["PUT"])
def update_file(id):
file = SpacyModel.query.get_or_404(id)
if "file" in request.files:
uploaded_file = request.files["file"]
if uploaded_file.filename != "":
file.filename = uploaded_file.filename
file_data = uploaded_file.read()
try:
if puremagic.from_string(file_data, mime=True) == "application/pdf":
file.file = file_data
except Exception as e:
print(e)
if "kpi" in request.form:
file.kpi = request.form.get("kpi")
db.session.commit()
return jsonify(file.to_dict()), 200
@spacy_controller.route("/<int:id>", methods=["DELETE"])
def delete_file(id):
file = SpacyModel.query.get_or_404(id)
db.session.delete(file)
db.session.commit()
return jsonify({"message": f"File {id} deleted successfully"}), 200
@spacy_controller.route("/append-training-entry", methods=["POST"])
def forward_training_entry():
entry = request.get_json()
try:
response = requests.post(f"{SPACY_URL}/append-training-entry", json=entry)
return jsonify(response.json()), response.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
# globale Variable oben einfügen
current_training_status = {"running": False}
@spacy_controller.route("/training/status", methods=["POST"])
def update_training_status():
data = request.get_json()
current_training_status["running"] = data.get("running", False)
if current_training_status["running"] is False:
try:
KPISettingModel.query.update({KPISettingModel.is_trained: True})
db.session.commit()
except Exception as e:
db.session.rollback()
return (
jsonify(
{
"error": "is_trained konnte nicht aktualisiert werden",
"details": str(e),
}
),
500,
)
return jsonify({"status": "success", "running": current_training_status["running"]})
@spacy_controller.route("/train-status", methods=["GET"])
def training_status():
return jsonify(current_training_status), 200

View File

@ -2,6 +2,8 @@ from model.database import db
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import Enum as SQLAlchemyEnum
from enum import Enum
from sqlalchemy.dialects.postgresql import JSONB
from collections import OrderedDict
class KPISettingType(Enum):
@ -18,37 +20,36 @@ class KPISettingModel(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(unique=True)
description: Mapped[str]
mandatory: Mapped[bool]
type: Mapped[KPISettingType] = mapped_column(
SQLAlchemyEnum(KPISettingType, native_enum=True)
)
translation: Mapped[str]
example: Mapped[str]
position: Mapped[int]
active: Mapped[bool]
examples: Mapped[list] = mapped_column(JSONB, default=[])
is_trained: Mapped[bool] = mapped_column(default=False)
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"description": self.description,
"mandatory": self.mandatory,
"type": self.type.value,
"translation": self.translation,
"example": self.example,
"position": self.position,
"active": self.active,
}
return OrderedDict(
[
("id", self.id),
("name", self.name),
("mandatory", self.mandatory),
("type", self.type.value),
("position", self.position),
("examples", self.examples),
("active", self.active),
("is_trained", self.is_trained),
]
)
def __init__(
self, name, description, mandatory, type, translation, example, position, active
self, name, mandatory, type, position, active, examples=None, is_trained=False
):
self.name = name
self.description = description
self.mandatory = mandatory
self.type = type
self.translation = translation
self.example = example
self.position = position
self.active = active
self.examples = examples or []
self.is_trained = is_trained

View File

@ -10,153 +10,258 @@ def seed_default_kpi_settings():
default_kpi_settings = [
{
"name": "Fondsname",
"description": "Der vollständige Name des Investmentfonds",
"mandatory": True,
"type": KPISettingType.STRING,
"translation": "Fund Name",
"example": "Alpha Real Estate Fund I",
"position": 1,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Der Fonds trägt den Namen Alpha Real Estate Fund I.",
"value": "Alpha Real Estate Fund I",
},
{
"sentence": "Im Pitchbook wird der Fondsname als Alpha Real Estate Fund I angegeben.",
"value": "Alpha Real Estate Fund I",
},
],
},
{
"name": "Fondsmanager",
"description": "Verantwortlicher Manager für die Fondsverwaltung",
"mandatory": True,
"type": KPISettingType.STRING,
"translation": "Fund Manager",
"example": "Max Mustermann",
"position": 2,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Fondsmanager des Projekts ist Max Mustermann.",
"value": "Max Mustermann",
},
{
"sentence": "Die Verwaltung liegt bei Max Mustermann.",
"value": "Max Mustermann",
},
],
},
{
"name": "AIFM",
"description": "Alternative Investment Fund Manager",
"mandatory": True,
"type": KPISettingType.STRING,
"translation": "AIFM",
"example": "Alpha Investment Management GmbH",
"position": 3,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "AIFM ist die Alpha Investment Management GmbH.",
"value": "Alpha Investment Management GmbH",
},
{
"sentence": "Die Alpha Investment Management GmbH fungiert als AIFM.",
"value": "Alpha Investment Management GmbH",
},
],
},
{
"name": "Datum",
"description": "Stichtag der Datenerfassung",
"mandatory": True,
"type": KPISettingType.DATE,
"translation": "Date",
"example": "05.05.2025",
"position": 4,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Die Daten basieren auf dem Stand vom 05.05.2025.",
"value": "05.05.2025",
},
{
"sentence": "Stichtag der Angaben ist der 05.05.2025.",
"value": "05.05.2025",
},
],
},
{
"name": "Risikoprofil",
"description": "Klassifizierung des Risikos des Fonds",
"mandatory": True,
"type": KPISettingType.STRING,
"translation": "Risk Profile",
"example": "Core/Core++",
"position": 5,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Der Fonds hat das Risikoprofil Core/Core++.",
"value": "Core/Core++",
},
{
"sentence": "Einstufung des Fondsrisikos: Core/Core++.",
"value": "Core/Core++",
},
],
},
{
"name": "Artikel",
"description": "Artikel 8 SFDR-Klassifizierung",
"mandatory": False,
"type": KPISettingType.BOOLEAN,
"translation": "Article",
"example": "Artikel 8",
"position": 6,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Der Fonds erfüllt die Anforderungen von Artikel 8.",
"value": "Artikel 8",
},
{
"sentence": "Gemäß SFDR fällt dieser Fonds unter Artikel 8.",
"value": "Artikel 8",
},
],
},
{
"name": "Zielrendite",
"description": "Angestrebte jährliche Rendite in Prozent",
"mandatory": True,
"type": KPISettingType.NUMBER,
"translation": "Target Return",
"example": "6.5",
"position": 7,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Die angestrebte Zielrendite liegt bei 6.5%.",
"value": "6.5%",
},
{"sentence": "Zielrendite des Fonds beträgt 6.5%.", "value": "6.5%"},
],
},
{
"name": "Rendite",
"description": "Tatsächlich erzielte Rendite in Prozent",
"mandatory": False,
"type": KPISettingType.NUMBER,
"translation": "Return",
"example": "5.8",
"position": 8,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Die Rendite für das Jahr beträgt 5.8%.",
"value": "5.8%",
},
{
"sentence": "Im letzten Jahr wurde eine Rendite von 5.8% erzielt.",
"value": "5.8%",
},
],
},
{
"name": "Zielausschüttung",
"description": "Geplante Ausschüttung in Prozent",
"mandatory": False,
"type": KPISettingType.NUMBER,
"translation": "Target Distribution",
"example": "4.0",
"position": 9,
"active": True,
"is_trained": True,
"examples": [
{"sentence": "Die Zielausschüttung beträgt 4.0%.", "value": "4.0%"},
{
"sentence": "Geplante Ausschüttung: 4.0% pro Jahr.",
"value": "4.0%",
},
],
},
{
"name": "Ausschüttung",
"description": "Tatsächliche Ausschüttung in Prozent",
"mandatory": False,
"type": KPISettingType.NUMBER,
"translation": "Distribution",
"example": "3.8",
"position": 10,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Die Ausschüttung im Jahr 2024 lag bei 3.8%.",
"value": "3.8%",
},
{
"sentence": "Es wurde eine Ausschüttung von 3.8% vorgenommen.",
"value": "3.8%",
},
],
},
{
"name": "Laufzeit",
"description": "Geplante Laufzeit des Fonds",
"mandatory": True,
"type": KPISettingType.STRING,
"translation": "Duration",
"example": "7 Jahre, 10, Evergreen",
"position": 11,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Die Laufzeit des Fonds beträgt 7 Jahre.",
"value": "7 Jahre",
},
{"sentence": "Geplante Dauer: Evergreen-Modell.", "value": "Evergreen"},
],
},
{
"name": "LTV",
"description": "Loan-to-Value Verhältnis in Prozent",
"mandatory": False,
"type": KPISettingType.NUMBER,
"translation": "LTV",
"example": "65.0",
"position": 12,
"active": True,
"is_trained": True,
"examples": [
{"sentence": "Der LTV beträgt 65.0%.", "value": "65.0%"},
{"sentence": "Loan-to-Value-Ratio: 65.0%.", "value": "65.0%"},
],
},
{
"name": "Managementgebühren",
"description": "Jährliche Verwaltungsgebühren in Prozent",
"mandatory": True,
"type": KPISettingType.NUMBER,
"translation": "Management Fees",
"example": "1.5",
"position": 13,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Die Managementgebühren betragen jährlich 1.5%.",
"value": "1.5%",
},
{
"sentence": "Für die Verwaltung wird eine Gebühr von 1.5% erhoben.",
"value": "1.5%",
},
],
},
{
"name": "Sektorenallokation",
"description": "Verteilung der Investments nach Sektoren",
"mandatory": False,
"type": KPISettingType.ARRAY,
"translation": "Sector Allocation",
"example": "Büro, Wohnen, Logistik, Studentenwohnen",
"position": 14,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Die Sektorenallokation umfasst Büro, Wohnen und Logistik.",
"value": "Büro, Wohnen, Logistik",
},
{
"sentence": "Investiert wird in Büro, Logistik und Studentenwohnen.",
"value": "Büro, Logistik, Studentenwohnen",
},
],
},
{
"name": "Länderallokation",
"description": "Geografische Verteilung der Investments",
"mandatory": False,
"type": KPISettingType.ARRAY,
"translation": "Country Allocation",
"example": "Deutschland,Frankreich, Österreich, Schweiz",
"position": 15,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Investitionen erfolgen in Deutschland, Frankreich und Österreich.",
"value": "Deutschland, Frankreich, Österreich",
},
{
"sentence": "Die Länderallokation umfasst Deutschland, Schweiz und Frankreich.",
"value": "Deutschland, Schweiz, Frankreich",
},
],
},
]
@ -165,13 +270,12 @@ def seed_default_kpi_settings():
for kpi_data in default_kpi_settings:
kpi_setting = KPISettingModel(
name=kpi_data["name"],
description=kpi_data["description"],
mandatory=kpi_data["mandatory"],
type=kpi_data["type"],
translation=kpi_data["translation"],
example=kpi_data["example"],
position=kpi_data["position"],
active=kpi_data["active"],
examples=kpi_data.get("examples", []),
is_trained=kpi_data["is_trained"],
)
db.session.add(kpi_setting)

View File

@ -6,9 +6,12 @@ import json
app = Flask(__name__)
VALIDATE_SERVICE_URL = os.getenv("VALIDATE_SERVICE_URL", "http://localhost:5054/validate")
VALIDATE_SERVICE_URL = os.getenv(
"VALIDATE_SERVICE_URL", "http://localhost:5054/validate"
)
@app.route('/extract', methods=['POST'])
@app.route("/extract", methods=["POST"])
def extract_text_from_ocr_json():
json_data = request.get_json()
@ -16,19 +19,19 @@ def extract_text_from_ocr_json():
pages_data = json_data["extracted_text_per_page"]
entities_json = extract_with_exxeta(pages_data, pitchbook_id)
entities = json.loads(entities_json) if isinstance(entities_json, str) else entities_json
entities = (
json.loads(entities_json) if isinstance(entities_json, str) else entities_json
)
validate_payload = {
"id": pitchbook_id,
"service": "exxeta",
"entities": entities
}
validate_payload = {"id": pitchbook_id, "service": "exxeta", "entities": entities}
print(f"[EXXETA] Sending to validate service: {VALIDATE_SERVICE_URL}")
print(f"[EXXETA] Payload: {validate_payload} entities for pitchbook {pitchbook_id}")
try:
response = requests.post(VALIDATE_SERVICE_URL, json=validate_payload, timeout=600)
response = requests.post(
VALIDATE_SERVICE_URL, json=validate_payload, timeout=600
)
print(f"[EXXETA] Validate service response: {response.status_code}")
if response.status_code != 200:
print(f"[EXXETA] Validate service error: {response.text}")

View File

@ -16,6 +16,7 @@ TIMEOUT = 180
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def get_dynamic_labels():
url = f"{COORDINATOR_URL}/api/kpi_setting/"
try:
@ -28,6 +29,7 @@ def get_dynamic_labels():
logger.warning(f"Konnte dynamische Labels nicht laden: {e}")
return []
def extract_with_exxeta(pages_json, pitchbook_id):
results = []
@ -39,7 +41,10 @@ def extract_with_exxeta(pages_json, pitchbook_id):
for page_data in pages_json:
i += 1
if i % 8 == 0:
requests.post(COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 35 + 60/len(pages_json)*i})
requests.post(
COORDINATOR_URL + "/api/progress",
json={"id": pitchbook_id, "progress": 35 + 60 / len(pages_json) * i},
)
page_num = page_data.get("page")
text = page_data.get("text", "")
@ -100,23 +105,28 @@ def extract_with_exxeta(pages_json, pitchbook_id):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {EXXETA_API_KEY}"
"Authorization": f"Bearer {EXXETA_API_KEY}",
}
payload = {
"model": MODEL,
"messages": [
{"role": "system", "content": "Du bist ein Finanzanalyst. Antworte ausschließlich mit einem validen JSON-Array."},
{"role": "user", "content": prompt}
{
"role": "system",
"content": "Du bist ein Finanzanalyst. Antworte ausschließlich mit einem validen JSON-Array.",
},
{"role": "user", "content": prompt},
],
"temperature": 0.0
"temperature": 0.0,
}
url = f"{EXXETA_BASE_URL}/deployments/{MODEL}/chat/completions"
for attempt in range(1, MAX_RETRIES + 1):
try:
response = requests.post(url, headers=headers, json=payload, timeout=TIMEOUT)
response = requests.post(
url, headers=headers, json=payload, timeout=TIMEOUT
)
response.raise_for_status()
content = response.json()["choices"][0]["message"]["content"].strip()
if content.startswith("```json"):
@ -140,9 +150,12 @@ def extract_with_exxeta(pages_json, pitchbook_id):
if attempt == MAX_RETRIES:
results.extend([])
requests.post(COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 95})
requests.post(
COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 95}
)
return json.dumps(results, indent=2, ensure_ascii=False)
if __name__ == "__main__":
print("📡 Test-Aufruf get_dynamic_labels:")
print(get_dynamic_labels())
print(get_dynamic_labels())

View File

@ -29,19 +29,17 @@ def convert_pdf_async(temp_path, pitchbook_id):
temp_path.unlink() # cleanup
return {"error": "OCR processing failed - all PDFs must be OCR'd"}, 500
with open(ocr_path, 'rb') as ocr_file:
with open(ocr_path, "rb") as ocr_file:
ocr_file.seek(0)
result = pdf_to_json(ocr_file)
payload = {
"id": int(pitchbook_id),
"extracted_text_per_page": result["pages"]
}
payload = {"id": int(pitchbook_id), "extracted_text_per_page": result["pages"]}
logger.info("Sending payload to EXXETA and SPACY services")
requests.post(COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 35})
requests.post(
COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 35}
)
try:
exxeta_response = requests.post(EXXETA_URL, json=payload, timeout=600)
logger.info(f"EXXETA response: {exxeta_response.status_code}")
@ -54,14 +52,16 @@ def convert_pdf_async(temp_path, pitchbook_id):
except Exception as e:
logger.error(f"Error calling SPACY: {e}")
files=[
('file',('',open(ocr_path,'rb'),'application/pdf'))
]
files = [("file", ("", open(ocr_path, "rb"), "application/pdf"))]
headers = {}
try:
requests.put(f"{COORDINATOR_URL}/api/pitch_book/{pitchbook_id}", files=files, timeout=600, headers=headers)
requests.put(
f"{COORDINATOR_URL}/api/pitch_book/{pitchbook_id}",
files=files,
timeout=600,
headers=headers,
)
logger.info("COORDINATOR response: Progress + File updated")
except Exception as e:
logger.error(f"Error calling COORDINATOR: {e}")
@ -72,7 +72,7 @@ def convert_pdf_async(temp_path, pitchbook_id):
logger.error(f"Exception in OCR processing: {str(e)}", exc_info=True)
@app.route('/ocr', methods=['POST'])
@app.route("/ocr", methods=["POST"])
def convert_extract_text_from_pdf():
if "file" not in request.files:
return {"error": "No file"}, 400
@ -85,7 +85,7 @@ def convert_extract_text_from_pdf():
if not pitchbook_id:
return {"error": "No ID"}, 400
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file:
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
file.seek(0)
temp_file.write(file.read())
temp_path = Path(temp_file.name)
@ -93,10 +93,7 @@ def convert_extract_text_from_pdf():
thread = threading.Thread(target=convert_pdf_async, args=(temp_path, pitchbook_id))
thread.start()
return {
"status": "sent",
"message": "PDF successfully OCR'd and processed"
}, 200
return {"status": "sent", "message": "PDF successfully OCR'd and processed"}, 200
if __name__ == "__main__":

View File

@ -17,9 +17,10 @@ log_folder = TEMP_DIR / "logs"
output_folder.mkdir(exist_ok=True)
log_folder.mkdir(exist_ok=True)
def pdf_to_json(pdf_input):
try:
if hasattr(pdf_input, 'read'):
if hasattr(pdf_input, "read"):
pdf_input.seek(0)
with pdfplumber.open(pdf_input) as pdf:
@ -83,7 +84,9 @@ def ocr_pdf(input_file_path: Path):
if result.returncode == 0:
if output_file.exists():
logger.info(f"OCR successful, output file size: {output_file.stat().st_size} bytes")
logger.info(
f"OCR successful, output file size: {output_file.stat().st_size} bytes"
)
return output_file
else:
logger.error(f"OCR completed but output file not found: {output_file}")
@ -119,4 +122,4 @@ def extract_text_to_json(pdf_path: Path):
except Exception as e:
logger.error(f"Failed to extract text to JSON: {e}")
return None
return None

View File

@ -11,6 +11,8 @@ COPY requirements.txt /app
RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install flask-cors
RUN python -m spacy download en_core_web_sm

View File

@ -1,14 +1,26 @@
from flask import Flask, request, jsonify
from extractSpacy import extract
from extractSpacy import extract, load_model
import requests
import os
import json
from flask_cors import CORS
import shutil
import subprocess
training_status = {"running": False}
app = Flask(__name__)
CORS(app)
VALIDATE_SERVICE_URL = os.getenv("VALIDATE_SERVICE_URL", "http://localhost:5054/validate")
COORDINATOR_URL = os.getenv("COORDINATOR_URL", "http://coordinator:5000")
VALIDATE_SERVICE_URL = os.getenv(
"VALIDATE_SERVICE_URL", "http://localhost:5054/validate"
)
@app.route('/extract', methods=['POST'])
@app.route("/extract", methods=["POST"])
def extract_pdf():
json_data = request.get_json()
@ -16,19 +28,19 @@ def extract_pdf():
pages_data = json_data["extracted_text_per_page"]
entities_json = extract(pages_data)
entities = json.loads(entities_json) if isinstance(entities_json, str) else entities_json
entities = (
json.loads(entities_json) if isinstance(entities_json, str) else entities_json
)
validate_payload = {
"id": pitchbook_id,
"service": "spacy",
"entities": entities
}
validate_payload = {"id": pitchbook_id, "service": "spacy", "entities": entities}
print(f"[SPACY] Sending to validate service: {VALIDATE_SERVICE_URL}")
print(f"[SPACY] Payload: {validate_payload} entities for pitchbook {pitchbook_id}")
try:
response = requests.post(VALIDATE_SERVICE_URL, json=validate_payload, timeout=600)
response = requests.post(
VALIDATE_SERVICE_URL, json=validate_payload, timeout=600
)
print(f"[SPACY] Validate service response: {response.status_code}")
if response.status_code != 200:
print(f"[SPACY] Validate service error: {response.text}")
@ -38,5 +50,90 @@ def extract_pdf():
return jsonify("Sent to validate-service"), 200
@app.route("/append-training-entry", methods=["POST"])
def append_training_entry():
entry = request.get_json()
if not entry or "text" not in entry or "entities" not in entry:
return (
jsonify(
{"error": "Ungültiges Format 'text' und 'entities' erforderlich."}
),
400,
)
path = os.path.join("spacy_training", "annotation_data.json")
try:
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
else:
data = []
# Duplikate prüfen
if entry in data:
return jsonify({"message": "Eintrag existiert bereits."}), 200
data.append(entry)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return jsonify({"message": "Eintrag erfolgreich gespeichert."}), 200
except Exception as e:
print(f"[ERROR] Fehler beim Schreiben der Datei: {e}")
return jsonify({"error": "Interner Fehler beim Schreiben."}), 500
@app.route("/train", methods=["POST"])
def trigger_training():
from threading import Thread
Thread(target=run_training).start()
return jsonify({"message": "Training gestartet"}), 200
@app.route("/reload-model", methods=["POST"])
def reload_model():
try:
load_model()
return jsonify({"message": "Modell wurde erfolgreich neu geladen."}), 200
except Exception as e:
return (
jsonify({"error": "Fehler beim Neuladen des Modells", "details": str(e)}),
500,
)
def run_training():
training_status["running"] = True
notify_coordinator(True)
try:
if os.path.exists("output/model-last"):
shutil.copytree(
"output/model-last", "output/model-backup", dirs_exist_ok=True
)
subprocess.run(["python", "spacy_training/ner_trainer.py"], check=True)
load_model()
except Exception as e:
print("Training failed:", e)
training_status["running"] = False
notify_coordinator(False)
def notify_coordinator(running: bool):
try:
response = requests.post(
f"{COORDINATOR_URL}/api/spacy/training/status", json={"running": running}
)
print(
f"[SPACY] Coordinator: running = {running}, Status = {response.status_code}"
)
except Exception as e:
print(f"[SPACY] Fehler beim Senden des Trainingsstatus: {e}")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5052, debug=True)
app.run(host="0.0.0.0", port=5052, debug=True)

View File

@ -2,9 +2,25 @@ import spacy
import os
import json
current_dir = os.path.dirname(os.path.abspath(__file__))
model_path = os.path.join(current_dir, "spacy_training/output/model-last")
nlp = spacy.load(model_path)
# Globales NLP-Modell
nlp = None
def load_model():
global nlp
print("[INFO] Lade SpaCy-Modell aus spacy_training/output/model-last ...")
nlp = spacy.load("spacy_training/output/model-last")
print("[INFO] Modell erfolgreich geladen.")
# Initial einmal laden
load_model()
def extract(pages_json):
results = []
@ -19,10 +35,6 @@ def extract(pages_json):
spacy_result = nlp(text)
for ent in spacy_result.ents:
results.append({
"label": ent.label_,
"entity": ent.text,
"page": page_num
})
results.append({"label": ent.label_, "entity": ent.text, "page": page_num})
return json.dumps(results, indent=2, ensure_ascii=False)
return json.dumps(results, indent=2, ensure_ascii=False)

View File

@ -3,4 +3,5 @@ spacy-transformers==1.3.3
transformers==4.35.2
torch
flask
requests
requests
flask-cors

View File

@ -0,0 +1,33 @@
from flask import Flask, request, jsonify
import os
import json
app = Flask(__name__)
ANNOTATION_FILE = "spacy_training/annotation_data.json"
@app.route("/api/spacy-training-entry", methods=["POST"])
def append_training_entry():
new_entry = request.get_json()
if not new_entry or "text" not in new_entry or "entities" not in new_entry:
return jsonify({"error": "Ungültiges Format"}), 400
# Bestehende Datei laden
if os.path.exists(ANNOTATION_FILE):
with open(ANNOTATION_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
else:
data = []
# Duplikat vermeiden
if new_entry in data:
return jsonify({"message": "Eintrag bereits vorhanden."}), 200
# Anfügen
data.append(new_entry)
with open(ANNOTATION_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return jsonify({"message": "Eintrag erfolgreich gespeichert."}), 200

File diff suppressed because it is too large Load Diff

View File

@ -1,85 +0,0 @@
# This is an auto-generated partial config. To use it with 'spacy train'
# you can run spacy init fill-config to auto-fill all default settings:
# python -m spacy init fill-config ./base_config.cfg ./config.cfg
[paths]
train = ./data/train.spacy
dev = ./data/train.spacy
vectors = null
[system]
gpu_allocator = null
[nlp]
lang = "de"
pipeline = ["tok2vec","ner"]
batch_size = 1000
[components]
[components.tok2vec]
factory = "tok2vec"
[components.tok2vec.model]
@architectures = "spacy.Tok2Vec.v2"
[components.tok2vec.model.embed]
@architectures = "spacy.MultiHashEmbed.v2"
width = ${components.tok2vec.model.encode.width}
attrs = ["NORM", "PREFIX", "SUFFIX", "SHAPE"]
rows = [5000, 1000, 2500, 2500]
include_static_vectors = false
[components.tok2vec.model.encode]
@architectures = "spacy.MaxoutWindowEncoder.v2"
width = 96
depth = 4
window_size = 1
maxout_pieces = 3
[components.ner]
factory = "ner"
[components.ner.model]
@architectures = "spacy.TransitionBasedParser.v2"
state_type = "ner"
extra_state_tokens = false
hidden_width = 64
maxout_pieces = 2
use_upper = true
nO = null
[components.ner.model.tok2vec]
@architectures = "spacy.Tok2VecListener.v1"
width = ${components.tok2vec.model.encode.width}
[corpora]
[corpora.train]
@readers = "spacy.Corpus.v1"
path = ${paths.train}
max_length = 0
[corpora.dev]
@readers = "spacy.Corpus.v1"
path = ${paths.dev}
max_length = 0
[training]
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
[training.optimizer]
@optimizers = "Adam.v1"
[training.batcher]
@batchers = "spacy.batch_by_words.v1"
discard_oversize = false
tolerance = 0.2
[training.batcher.size]
@schedules = "compounding.v1"
start = 100
stop = 1000
compound = 1.001
[initialize]
vectors = ${paths.vectors}

View File

@ -0,0 +1,18 @@
import json
# Alte Daten laden
with open("annotation_data.json", "r", encoding="utf-8") as f:
data = json.load(f)
# Neue Kennzahl (als Dict/Objekt)
neuer_eintrag = {
"text": "Hier steht der Beispielsatz mit der neuen Kennzahl.",
"entities": [[1, 5, "NEUEKENNZAHL"]],
}
# Anhängen
data.append(neuer_eintrag)
# Wieder speichern
with open("annotation_data.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)

View File

@ -0,0 +1,81 @@
import spacy
from spacy.training.example import Example
import json
import os
import shutil
import sys
def load_data(file_path):
with open(file_path, "r", encoding="utf8") as f:
raw = json.load(f)
return [
(
entry["text"],
{
"entities": [
(start, end, label) for start, end, label in entry["entities"]
]
},
)
for entry in raw
]
def main():
# Stelle sicher, dass der "output"-Ordner existiert
os.makedirs("output", exist_ok=True)
TRAIN_DATA = load_data(os.path.join("spacy_training", "annotation_data.json"))
nlp = spacy.blank("de")
ner = nlp.add_pipe("ner")
ner.add_label("KENNZAHL")
optimizer = nlp.begin_training()
for i in range(20):
for text, annotations in TRAIN_DATA:
example = Example.from_dict(nlp.make_doc(text), annotations)
nlp.update([example], drop=0.2, sgd=optimizer)
temp_model_dir = "output/temp-model"
final_model_dir = "output/model-last"
backup_dir = "output/model-backup"
try:
# Vorheriges temporäres Verzeichnis entfernen
if os.path.exists(temp_model_dir):
shutil.rmtree(temp_model_dir)
# Modell zunächst in temp speichern
nlp.to_disk(temp_model_dir)
# Backup der letzten Version (falls vorhanden)
if os.path.exists(final_model_dir):
if os.path.exists(backup_dir):
shutil.rmtree(backup_dir)
shutil.copytree(final_model_dir, backup_dir)
shutil.rmtree(final_model_dir)
# Modell verschieben
shutil.move(temp_model_dir, final_model_dir)
print("[INFO] Training abgeschlossen und Modell gespeichert.")
nlp.to_disk("spacy_training/output/model-last")
# Training beendet Status auf False setzen
with open("spacy_training/training_running.json", "w") as f:
json.dump({"running": False}, f)
sys.exit(0)
except Exception as e:
print(f"[FEHLER] Während des Trainings ist ein Fehler aufgetreten: {e}")
if os.path.exists(temp_model_dir):
shutil.rmtree(temp_model_dir)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -1,21 +1,21 @@
[paths]
train = "./data/train.spacy"
dev = "./data/train.spacy"
train = null
dev = null
vectors = null
init_tok2vec = null
[system]
gpu_allocator = null
seed = 0
gpu_allocator = null
[nlp]
lang = "de"
pipeline = ["tok2vec","ner"]
batch_size = 1000
pipeline = ["ner"]
disabled = []
before_creation = null
after_creation = null
after_pipeline_creation = null
batch_size = 1000
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}
vectors = {"@vectors":"spacy.Vectors.v1"}
@ -38,51 +38,34 @@ use_upper = true
nO = null
[components.ner.model.tok2vec]
@architectures = "spacy.Tok2VecListener.v1"
width = ${components.tok2vec.model.encode.width}
upstream = "*"
[components.tok2vec]
factory = "tok2vec"
[components.tok2vec.model]
@architectures = "spacy.Tok2Vec.v2"
[components.tok2vec.model.embed]
@architectures = "spacy.MultiHashEmbed.v2"
width = ${components.tok2vec.model.encode.width}
attrs = ["NORM","PREFIX","SUFFIX","SHAPE"]
rows = [5000,1000,2500,2500]
include_static_vectors = false
[components.tok2vec.model.encode]
@architectures = "spacy.MaxoutWindowEncoder.v2"
@architectures = "spacy.HashEmbedCNN.v2"
pretrained_vectors = null
width = 96
depth = 4
embed_size = 2000
window_size = 1
maxout_pieces = 3
subword_features = true
[corpora]
[corpora.dev]
@readers = "spacy.Corpus.v1"
path = ${paths.dev}
max_length = 0
gold_preproc = false
max_length = 0
limit = 0
augmenter = null
[corpora.train]
@readers = "spacy.Corpus.v1"
path = ${paths.train}
max_length = 0
gold_preproc = false
max_length = 0
limit = 0
augmenter = null
[training]
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
seed = ${system.seed}
gpu_allocator = ${system.gpu_allocator}
dropout = 0.1
@ -93,6 +76,8 @@ max_steps = 20000
eval_frequency = 200
frozen_components = []
annotating_components = []
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
before_to_disk = null
before_update = null

View File

@ -0,0 +1,63 @@
{
"lang":"de",
"name":"pipeline",
"version":"0.0.0",
"spacy_version":">=3.8.7,<3.9.0",
"description":"",
"author":"",
"email":"",
"url":"",
"license":"",
"spacy_git_version":"4b65aa7",
"vectors":{
"width":0,
"vectors":0,
"keys":0,
"name":null,
"mode":"default"
},
"labels":{
"ner":[
"AUSSCH\u00dcTTUNGSRENDITE",
"ESFCG",
"HHGK",
"KENNZAHL",
"LAUFZEIT",
"LAUFZEIT2",
"L\u00c4NDERALLOKATION",
"MANAGMENTGEB\u00dcHREN",
"RENDITE",
"RISIKOPROFIL",
"SEKTORENALLOKATION",
"TEST",
"TEST3",
"TEST323",
"TEST33",
"TEST34",
"TEST345",
"TEST35",
"TEST36",
"TEST37",
"TEST4",
"TEST44",
"TEST45",
"TEST5",
"TEST65",
"TEST67",
"TEST88",
"TEST99",
"ZIELAUSSCH\u00dcTTUNG",
"ZIELRENDITE",
"ZZGER 33"
]
},
"pipeline":[
"ner"
],
"components":[
"ner"
],
"disabled":[
]
}

View File

@ -0,0 +1,13 @@
{
"moves":null,
"update_with_oracle_cut_size":100,
"multitasks":[
],
"min_action_freq":1,
"learn_tokens":false,
"beam_width":1,
"beam_density":0.0,
"beam_update_prob":0.0,
"incorrect_spans_key":null
}

View File

@ -0,0 +1 @@
¥movesÚÈ{"0":{},"1":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"2":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"3":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"4":{"":1,"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"5":{"":1}}£cfg<66>§neg_keyÀ

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
{
"mode":"default"
}

View File

@ -1,21 +1,21 @@
[paths]
train = "./data/train.spacy"
dev = "./data/train.spacy"
train = null
dev = null
vectors = null
init_tok2vec = null
[system]
gpu_allocator = null
seed = 0
gpu_allocator = null
[nlp]
lang = "de"
pipeline = ["tok2vec","ner"]
batch_size = 1000
pipeline = ["ner"]
disabled = []
before_creation = null
after_creation = null
after_pipeline_creation = null
batch_size = 1000
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}
vectors = {"@vectors":"spacy.Vectors.v1"}
@ -38,51 +38,34 @@ use_upper = true
nO = null
[components.ner.model.tok2vec]
@architectures = "spacy.Tok2VecListener.v1"
width = ${components.tok2vec.model.encode.width}
upstream = "*"
[components.tok2vec]
factory = "tok2vec"
[components.tok2vec.model]
@architectures = "spacy.Tok2Vec.v2"
[components.tok2vec.model.embed]
@architectures = "spacy.MultiHashEmbed.v2"
width = ${components.tok2vec.model.encode.width}
attrs = ["NORM","PREFIX","SUFFIX","SHAPE"]
rows = [5000,1000,2500,2500]
include_static_vectors = false
[components.tok2vec.model.encode]
@architectures = "spacy.MaxoutWindowEncoder.v2"
@architectures = "spacy.HashEmbedCNN.v2"
pretrained_vectors = null
width = 96
depth = 4
embed_size = 2000
window_size = 1
maxout_pieces = 3
subword_features = true
[corpora]
[corpora.dev]
@readers = "spacy.Corpus.v1"
path = ${paths.dev}
max_length = 0
gold_preproc = false
max_length = 0
limit = 0
augmenter = null
[corpora.train]
@readers = "spacy.Corpus.v1"
path = ${paths.train}
max_length = 0
gold_preproc = false
max_length = 0
limit = 0
augmenter = null
[training]
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
seed = ${system.seed}
gpu_allocator = ${system.gpu_allocator}
dropout = 0.1
@ -93,6 +76,8 @@ max_steps = 20000
eval_frequency = 200
frozen_components = []
annotating_components = []
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
before_to_disk = null
before_update = null

View File

@ -17,11 +17,9 @@
"mode":"default"
},
"labels":{
"tok2vec":[
],
"ner":[
"AUSSCH\u00dcTTUNGSRENDITE",
"KENNZAHL",
"LAUFZEIT",
"L\u00c4NDERALLOKATION",
"MANAGMENTGEB\u00dcHREN",
@ -33,68 +31,12 @@
]
},
"pipeline":[
"tok2vec",
"ner"
],
"components":[
"tok2vec",
"ner"
],
"disabled":[
],
"performance":{
"ents_f":0.9608938547,
"ents_p":1.0,
"ents_r":0.9247311828,
"ents_per_type":{
"RISIKOPROFIL":{
"p":1.0,
"r":1.0,
"f":1.0
},
"AUSSCH\u00dcTTUNGSRENDITE":{
"p":1.0,
"r":0.5925925926,
"f":0.7441860465
},
"LAUFZEIT":{
"p":1.0,
"r":1.0,
"f":1.0
},
"RENDITE":{
"p":1.0,
"r":1.0,
"f":1.0
},
"L\u00c4NDERALLOKATION":{
"p":1.0,
"r":0.8965517241,
"f":0.9454545455
},
"ZIELRENDITE":{
"p":1.0,
"r":1.0,
"f":1.0
},
"ZIELAUSSCH\u00dcTTUNG":{
"p":1.0,
"r":1.0,
"f":1.0
},
"MANAGMENTGEB\u00dcHREN":{
"p":1.0,
"r":1.0,
"f":1.0
},
"SEKTORENALLOKATION":{
"p":1.0,
"r":1.0,
"f":1.0
}
},
"tok2vec_loss":33.6051129291,
"ner_loss":740.5764770508
}
]
}

View File

@ -1 +1 @@
¥movesÚL{"0":{},"1":{"RISIKOPROFIL":161,"L\u00c4NDERALLOKATION":161,"RENDITE":91,"AUSSCH\u00dcTTUNGSRENDITE":68,"LAUFZEIT":38,"ZIELRENDITE":12,"SEKTORENALLOKATION":12,"MANAGMENTGEB\u00dcHREN":8,"ZIELAUSSCH\u00dcTTUNG":2},"2":{"RISIKOPROFIL":161,"L\u00c4NDERALLOKATION":161,"RENDITE":91,"AUSSCH\u00dcTTUNGSRENDITE":68,"LAUFZEIT":38,"ZIELRENDITE":12,"SEKTORENALLOKATION":12,"MANAGMENTGEB\u00dcHREN":8,"ZIELAUSSCH\u00dcTTUNG":2},"3":{"RISIKOPROFIL":161,"L\u00c4NDERALLOKATION":161,"RENDITE":91,"AUSSCH\u00dcTTUNGSRENDITE":68,"LAUFZEIT":38,"ZIELRENDITE":12,"SEKTORENALLOKATION":12,"MANAGMENTGEB\u00dcHREN":8,"ZIELAUSSCH\u00dcTTUNG":2},"4":{"RISIKOPROFIL":161,"L\u00c4NDERALLOKATION":161,"RENDITE":91,"AUSSCH\u00dcTTUNGSRENDITE":68,"LAUFZEIT":38,"ZIELRENDITE":12,"SEKTORENALLOKATION":12,"MANAGMENTGEB\u00dcHREN":8,"ZIELAUSSCH\u00dcTTUNG":2,"":1},"5":{"":1}}£cfg<66>§neg_keyÀ
¥movesÚˆ{"0":{},"1":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10},"2":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10},"3":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10},"4":{"":1,"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10},"5":{"":1}}£cfg<66>§neg_keyÀ

View File

@ -52,9 +52,7 @@
"*",
"+",
"+/D",
"+/d",
"+AU",
"+au",
",",
",00",
",03",
@ -129,21 +127,15 @@
"/080%/212%/491",
"/2,12",
"/3",
"/AuM",
"/Core+",
"/FK",
"/XX",
"/XxX",
"/Xxxx+",
"/aum",
"/core+",
"/d",
"/d,dd",
"/ddd%/ddd%/ddd",
"/fk",
"/xx",
"/xxx",
"/xxxx+",
"0",
"0%+",
"0,0",
@ -281,7 +273,6 @@
"45",
"491",
"5",
"5%+",
"5,0",
"5,00",
"5,1",
@ -311,7 +302,6 @@
"7",
"7,1",
"7,5",
"7,5%+",
"7,50",
"7,50%+",
"7.5",
@ -328,8 +318,6 @@
"8-D",
"8-d",
"80",
"800",
"84,0",
"85",
"8D",
"8d",
@ -469,7 +457,6 @@
"Abteilung",
"Access",
"Add",
"Agreements",
"Aktive",
"Aktueller",
"AlF",
@ -477,7 +464,6 @@
"Allocation",
"Allokation",
"Allokationsprofil",
"Alternative",
"Amsterdam",
"Andere",
"Anfrage",
@ -513,7 +499,6 @@
"Artikel",
"Asset",
"Assets",
"AuM",
"Aufbau",
"Auflage",
"Aufl\u00f6sung",
@ -583,10 +568,7 @@
"COR",
"CORE",
"CSU",
"CSp",
"Cash",
"Cash-Flow-Stabilit\u00e4t",
"Cash-flow",
"Chr",
"Chr.",
"Cie",
@ -648,7 +630,6 @@
"E.",
"EAN",
"ECLF",
"EIT",
"EM",
"ERD",
"ESG-",
@ -683,7 +664,6 @@
"F",
"F.",
"FDR",
"FIL",
"FR",
"FRANCE",
"FUND",
@ -794,11 +774,9 @@
"III.",
"INK",
"INREV",
"ION",
"IRR",
"IRR6.5",
"IT",
"ITE",
"IUM",
"IV",
"IV.",
@ -824,7 +802,6 @@
"Interner",
"Invastitionsfokus",
"Investftionsvolumen",
"Investing",
"Investitionen",
"Investitions-annahmen",
"Investitionsphase",
@ -834,7 +811,6 @@
"Investmentzeitraum",
"Investoren",
"Investtionszeltraum",
"Investtionszeltraum,10",
"Investtonszeltraum",
"Ireland",
"Irland",
@ -866,6 +842,7 @@
"K.",
"K.O.",
"KAGB",
"KENNZAHL",
"KINGDOM",
"KVG",
"Kapitalstruktur",
@ -905,7 +882,6 @@
"Light",
"Limited",
"Lisbon",
"Loan-to-Value",
"Local",
"Logistics",
"Logistik",
@ -1056,12 +1032,10 @@
"Prof",
"Prof.",
"Professor",
"Prognose",
"Prognostiderte",
"Prognostizierte",
"Projektentwicklungen",
"Projektentwicklungsrisiken",
"PropCo",
"Pt",
"Punkt",
"Q",
@ -1072,7 +1046,6 @@
"R.",
"R.I.P.",
"RE",
"REN",
"RENDITE",
"REV",
"REWE",
@ -1104,7 +1077,6 @@
"S",
"S'",
"SCS",
"SCSp",
"SEKTORENALLOKATION",
"SFDR",
"SG-",
@ -1197,7 +1169,6 @@
"U.S.S.",
"UK",
"UND",
"UNG",
"UNITED",
"USt",
"Univ",
@ -1264,7 +1235,6 @@
"XXXX",
"XXXX-XXXX",
"XXXd.d",
"XXXx",
"XXXxx",
"XXx",
"XXxxxx",
@ -1289,18 +1259,15 @@
"Xxxx-XXX",
"Xxxx-Xxxx-Xxxxx",
"Xxxx-Xxxxx-XXX",
"Xxxx-xx-Xxxxx",
"Xxxx-xxx",
"Xxxx-xxxx",
"Xxxx.",
"Xxxx.-Xxx",
"Xxxx.-Xxx.",
"XxxxXx",
"Xxxxx",
"Xxxxx)-",
"Xxxxx)/Xxxx",
"Xxxxx+",
"Xxxxx,dd",
"Xxxxx-",
"Xxxxx-XXX",
"Xxxxx-XxX",
@ -1381,6 +1348,7 @@
"a.g.",
"a.m.",
"a.z.",
"a34",
"ab",
"abb",
"abb.",
@ -1410,14 +1378,11 @@
"advantage",
"ae",
"aft",
"agb",
"age",
"agreements",
"aha",
"ahe",
"ahl",
"ahr",
"aif",
"ail",
"aiming",
"ain",
@ -1429,7 +1394,6 @@
"al.",
"ald",
"ale",
"alf",
"all",
"allg",
"allg.",
@ -1440,8 +1404,6 @@
"allokationsprofil",
"als",
"also",
"alt",
"alternative",
"aly",
"am.",
"ambulant",
@ -1497,7 +1459,6 @@
"ary",
"as",
"ase",
"ash",
"ass",
"asset",
"assetor",
@ -1535,7 +1496,6 @@
"aussch\u00fcttungsrandite",
"aussch\u00fcttungsrendite",
"aussch\u00fcttungsrendites",
"aut",
"ave",
"ax.",
"b",
@ -1544,7 +1504,6 @@
"b.sc",
"b.sc.",
"bahnhof",
"bal",
"balanced",
"basis",
"bau",
@ -1586,7 +1545,6 @@
"both",
"bps",
"br.",
"brands",
"broad",
"brussels",
"bruttofondsverm\u00f6gens",
@ -1619,8 +1577,6 @@
"capital",
"capped",
"carbon",
"cash",
"cash-flow",
"cash-flow-stabilit\u00e4t",
"cbd",
"cdu",
@ -1643,7 +1599,6 @@
"cl.",
"class",
"cle",
"clf",
"closed",
"closing",
"closings",
@ -1663,7 +1618,6 @@
"construction",
"contract",
"contracts",
"cor",
"core",
"core+",
"core+/d",
@ -1672,7 +1626,6 @@
"could",
"country",
"creation",
"csp",
"csu",
"cts",
"currency",
@ -1684,7 +1637,6 @@
"d+au",
"d+aut",
"d,d",
"d,d%+",
"d,dd",
"d,dd%+",
"d,ddd",
@ -1710,6 +1662,7 @@
"darge",
"darlehen",
"das",
"dasda34",
"dat",
"dd",
"dd,d",
@ -1735,7 +1688,6 @@
"der",
"dergleichen",
"des",
"destinations",
"deutsche",
"deutsches",
"deutschland",
@ -1756,7 +1708,6 @@
"dipl.",
"dipl.-ing",
"dipl.-ing.",
"dis",
"discretionary",
"distributions",
"diversification",
@ -1767,8 +1718,6 @@
"dle",
"do",
"do.",
"dom",
"domicile",
"domiciled",
"don",
"down",
@ -1783,7 +1732,6 @@
"durchschnittlich",
"du\u2019s",
"dv.",
"dxxx.\u20ac",
"dy",
"d\u00e4nemark",
"d\u2019",
@ -1847,7 +1795,6 @@
"eln",
"els",
"elt",
"ely",
"em",
"em.",
"emerging",
@ -1877,7 +1824,6 @@
"er.",
"erb",
"erbbaurechte",
"erd",
"ere",
"erfolgten",
"erg",
@ -1902,7 +1848,6 @@
"ete",
"etr",
"ets",
"eturn",
"eu-offenlegungsverordnung",
"eur",
"euro",
@ -1940,7 +1885,6 @@
"fam",
"fam.",
"favour",
"fdr",
"feb",
"feb.",
"fee",
@ -1952,7 +1896,6 @@
"ff",
"fierce",
"fil",
"financially",
"finanzierung",
"finanzierungskonditionen",
"finland",
@ -1960,7 +1903,6 @@
"first",
"flagship",
"flow",
"flow-oriented",
"fl\u00e4che",
"focus",
"focused",
@ -2051,7 +1993,6 @@
"gic",
"gie",
"gl.",
"global",
"globale",
"gmbh",
"goal",
@ -2231,7 +2172,6 @@
"investoren",
"investors",
"investtionszeltraum",
"investtionszeltraum,10",
"investtonszeltraum",
"inw",
"io.",
@ -2374,8 +2314,6 @@
"lls",
"llt",
"llv",
"lly",
"loan-to-value",
"local",
"locations",
"lock-in",
@ -2383,7 +2321,6 @@
"logistik",
"logistikimmobilien",
"london",
"long-term",
"low",
"lps",
"lso",
@ -2393,7 +2330,6 @@
"lto",
"ltv",
"ltv-ziel",
"lty",
"lu",
"lub",
"lue",
@ -2427,7 +2363,6 @@
"management",
"manager",
"manager-defined",
"managmentgeb\u00fchren",
"mandate",
"mandates",
"market",
@ -2439,7 +2374,6 @@
"maximal",
"maximaler",
"mbH",
"mbh",
"means",
"medizin",
"medizinnahe",
@ -2659,11 +2593,7 @@
"parformanceabh\u00e4ngige",
"paris",
"parks",
"partners",
"partnership",
"pattern",
"pci",
"pco",
"ped",
"pen",
"per",
@ -2694,18 +2624,15 @@
"pricey",
"pricing",
"prime",
"pro",
"prof",
"prof.",
"profile",
"prognose",
"prognostiderte",
"prognostizierte",
"program",
"projects",
"projektentwicklungen",
"projektentwicklungsrisiken",
"propco",
"properties",
"proprietary",
"provide",
@ -2719,7 +2646,6 @@
"q.",
"q.e.d",
"q.e.d.",
"qin",
"quality",
"quarterly",
"quota",
@ -2756,7 +2682,6 @@
"relationships",
"remains",
"ren",
"rendite",
"rendite-",
"rendite-risiko-profil",
"renegotiation",
@ -2773,7 +2698,6 @@
"retailinvestitionsvolumen",
"return",
"returns",
"rev",
"reversion",
"rewe",
"rge",
@ -2800,12 +2724,10 @@
"rop",
"rotterdam",
"rr.",
"rre",
"rs.",
"rsg",
"rst",
"rte",
"rtt",
"rz.",
"r\u00f6m",
"r\u00f6m.",
@ -2826,7 +2748,6 @@
"schweden",
"scope",
"scs",
"scsp",
"sd.",
"sector",
"sectors",
@ -2835,7 +2756,6 @@
"segment",
"sektor",
"sektoraler",
"sektorenallokation",
"selection",
"sen",
"sen.",
@ -2849,7 +2769,6 @@
"set",
"sf.",
"sfdr",
"sg-",
"sg.",
"short-term",
"sicav-raif",
@ -2870,7 +2789,6 @@
"sofern",
"sog",
"sog.",
"solely",
"solvency",
"some",
"son",
@ -3128,9 +3046,6 @@
"worldwide",
"x",
"x'",
"x+xx",
"x+xxx",
"x-xxxx",
"x.",
"x.X",
"x.X.",
@ -3157,38 +3072,23 @@
"xemoours",
"xit",
"xx",
"xx-xxxx",
"xx.",
"xx.x",
"xxXxx",
"xxx",
"xxx-",
"xxx-Xxxxx",
"xxx-xxxx",
"xxx.",
"xxxd.d",
"xxxx",
"xxxx)-",
"xxxx)/xxxx",
"xxxx+",
"xxxx+/x",
"xxxx+/xxxx",
"xxxx,dd",
"xxxx-",
"xxxx-xx",
"xxxx-xx-xxxx",
"xxxx-xxx",
"xxxx-xxxx",
"xxxx-xxxx-xxx",
"xxxx-xxxx-xxxx",
"xxxx.",
"xxxx\u0308xx",
"xxxx\u0308xxx-xxxx",
"xxxx\u0308xxxx",
"xxxxdd",
"xxxx\u2019x",
"xxx\u2019x",
"xx\u0308x",
"xx\u0308xxxx",
"xx\u2019x",
"x\u0308xxx",
"x\u0308xxxx",
@ -3224,7 +3124,6 @@
"zielallokation",
"zielanlagestrategie",
"zielausschu\u0308ttung",
"zielaussch\u00fcttung",
"zielmarkts",
"zielm\u00e4rkte",
"zielobjekte",

View File

@ -0,0 +1,122 @@
[
{
"text": "Core",
"entities": [
[
0,
4,
"RISIKOPROFIL"
]
]
},
{
"text": "Core+",
"entities": [
[
0,
5,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Core+",
"entities": [
[
0,
10,
"RISIKOPROFIL"
]
]
},
{
"text": "Value Add",
"entities": [
[
0,
9,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Value Add",
"entities": [
[
0,
14,
"RISIKOPROFIL"
]
]
},
{
"text": "Core+/Value Add",
"entities": [
[
0,
15,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Core+/Value Add",
"entities": [
[
0,
20,
"RISIKOPROFIL"
]
]
},
{
"text": "The RE portfolio of the fund is a good illustration of Fond expertise in European core/core+ investments .",
"entities": [
[
82,
92,
"RISIKOPROFIL"
]
]
},
{
"text": "Risk level: Core/Core+",
"entities": [
[
12,
22,
"RISIKOPROFIL"
]
]
},
{
"text": "Different risk profile (core, core+, value-added)",
"entities": [
[
24,
48,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Core+ with OpCo premium",
"entities": [
[
0,
10,
"RISIKOPROFIL"
]
]
},
{
"text": "Core /Core+ Assets, well-established = Key Gateway Cities in Europe le.g. hotels in the market with minor asset London, Paris, Amsterdam, Berlin] management initiatives",
"entities": [
[
0,
11,
"RISIKOPROFIL"
]
]
}
]

View File

@ -1,563 +0,0 @@
TRAINING_DATA = [
(
"Core",
{"entities": [[0, 4, "RISIKOPROFIL"]]},
),
(
"Core+",
{"entities": [[0, 5, "RISIKOPROFIL"]]},
),
(
"Core/Core+",
{"entities": [[0, 10, "RISIKOPROFIL"]]},
),
(
"Value Add",
{"entities": [[0, 9, "RISIKOPROFIL"]]},
),
(
"Core/Value Add",
{"entities": [[0, 14, "RISIKOPROFIL"]]},
),
(
"Core+/Value Add",
{"entities": [[0, 15, "RISIKOPROFIL"]]},
),
(
"Core/Core+/Value Add",
{"entities": [[0, 20, "RISIKOPROFIL"]]},
),
(
"The RE portfolio of the fund is a good illustration of Fond expertise in European core/core+ investments .",
{"entities": [[82, 92, "RISIKOPROFIL"]]},
),
(
"Risk level: Core/Core+",
{"entities": [[12, 22, "RISIKOPROFIL"]]},
),
(
"Different risk profile (core, core+, value-added)",
{"entities": [[24, 48, "RISIKOPROFIL"]]},
),
(
"Core/Core+ with OpCo premium",
{"entities": [[0, 10, "RISIKOPROFIL"]]},
),
(
"Core /Core+ Assets, well-established = Key Gateway Cities in Europe le.g. hotels in the market with minor asset London, Paris, Amsterdam, Berlin] management initiatives",
{"entities": [[0, 11, "RISIKOPROFIL"]]},
),
(
"Risikoprofil: Core, Core +",
{"entities": [[14, 26, "RISIKOPROFIL"]]},
),
(
"Name des Fonds Name des Investmentmanagers Allgemeine Informationen Name des Ansprechpartners Telefonnummer des Ansprechpartners E-Mail des Ansprechpartners Art des Anlagevehikels Struktur des Anlagevehikels Sitz des Anlagevehikels Struktur des Antagevehikels vom Manager festgelegter Stil Rechtsform Jahr des ersten Closings Laufzeit Geplantes Jahr der Auflösung Ziel-Netto-IRR / Gesamtrendite* Zielvolumen des Anlagevehikels Ziel-LTY Aktueller LTV Ziirraiaein Maximaler LTV Zielregionfen)/Jand Zielsektoren Zielanlagestrategie INREV Fonds Offen Deutschland Core, Core + Offener Immobilien-Spezialfonds 2022 10 - 12 Jahre 2032 - 2034 7,50%+ 250 Mio. € 20% 0% 20% Führende Metropolregionen Deutschlands und ausgewählte Standorte >50T Einw. Wohnimmobilien Wertstabile Wohnimmobilien (mit Bestandsentwicklungen)",
{"entities": [[560, 572, "RISIKOPROFIL"]]},
),
(
"Core/Core+ strategy, with tactical exposure to development projects aiming at enhancing the quality of the portfolio over time",
{"entities": [[0, 10, "RISIKOPROFIL"]]},
),
(
"Strategie - Übersicht Risikoprofil Core+ Halten-Strategie Kaufen — Halten (langfristig) — Exit 1. Nachvermietungsstrategie Anlagestrategien 2. Standortaufwertungsstrategie 3. Strategie der Aufwertung der Immobilien Niederlande (max. 35 %) Länderallokation Frankreich (max. 35 %) (in % vom Zielvolumen) Skandinavien (Schweden, Dänemark) (max. 35 %) Deutschland (<= 10 %)",
{"entities": [[35, 40, "RISIKOPROFIL"]]},
),
(
"Core and Core+",
{"entities": [[0, 14, "RISIKOPROFIL"]]},
),
(
"core, core+, value-added",
{"entities": [[0, 24, "RISIKOPROFIL"]]},
),
(
"Manage to Core: max 20%",
{"entities": [[10, 14, "RISIKOPROFIL"]]},
),
(
"Benefits of the core/ core+ segment",
{"entities": [[16, 27, "RISIKOPROFIL"]]},
),
(
"Drawbacks of the core/ core+ segment",
{"entities": [[17, 28, "RISIKOPROFIL"]]},
),
(
"Why a Core / Core + investment program?",
{"entities": [[6, 19, "RISIKOPROFIL"]]},
),
(
"Different risk profile (core, core+, value-added)",
{"entities": [[24, 48, "RISIKOPROFIL"]]},
),
(
"INK MGallery Hotel Area: Amsterdam Core Tenant: Closed in 2018",
{"entities": [[35, 39, "RISIKOPROFIL"]]},
),
(
"A strategy targeting high quality Core and Core+ buildings, with defined SRI objectives, in order to extract value through an active asset management.",
{"entities": [[34, 48, "RISIKOPROFIL"]]},
),
(
"Navigate the diversity of the Core/Core+ investment opportunities in European Prime Cities",
{"entities": [[30, 40, "RISIKOPROFIL"]]},
),
(
"GEDis an open-ended Lux-based fund providing an attractive core/core+ real estate exposure, leveraging GRRE expertise in European RE markets. It offers diversification in terms of pan-European geographies and sectors: Offices, Retail and Hotels.",
{"entities": [[59, 69, "RISIKOPROFIL"]]},
),
(
"Core assets leave less room for active asset management value creation",
{"entities": [[0, 4, "RISIKOPROFIL"]]},
),
(
"capital preservation is defined here as a characteristic of core/core+ investments. There is no guarantee of capital.",
{"entities": [[60, 70, "RISIKOPROFIL"]]},
),
(
"Country / city BELGIUM Brussels BELGIUM Brussels SPAIN Madrid FRANCE Levallois FRANCE Paris 14 BELGIUM Brussels NETHERLANDS Rotterdam NETHERLANDS Rotterdam Sector Offices Offices Offices Offices Offices Offices Offices Logistics Risk Core",
{"entities": [[234, 238, "RISIKOPROFIL"]]},
),
(
"GERD(a balanced pan-European open ended retail fund — under the form of a French collective undertaking for Real Estate investments “OPCI”) is the flagship ofQin France and combines RE and listed assets (respective targets of 60% and 40%) with max. 40% leverage. The RE portfolio of the fund is a good illustration Of expertise in European core/core+ investments.",
{"entities": [[340, 350, "RISIKOPROFIL"]]},
),
(
"Prime office assets in Prime markets are very pricey unless rent reversion is real. Risk premium remains attractive on a leveraged basis. Manage to core or build to core can make sense as a LT investor in main cities. Residential is also attractive",
{"entities": [[148, 152, "RISIKOPROFIL"]]},
),
(
"Paris region is a deep and liquid market. Rents have some potential to improve. Considering current low yield and fierce competition, office right outside CBD for Core + assets can be considered. Manage to core strategies could make sense.",
{"entities": [[163, 169, "RISIKOPROFIL"]]},
),
(
"Lisbon is a small market but it experienced a rapid economic recovery in recent years and is interesting for Core Offices, quality Retail assetor Hotel walls with top operators. Limited liquidity of this market means investment must be small",
{"entities": [[109, 113, "RISIKOPROFIL"]]},
),
(
"4,0 %",
{"entities": [[0, 5, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Prognostizierte jährliche Ausschüttung von 4,0%",
{"entities": [[44, 48, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"20% über einer @ Ausschüttungsrendite von 4,0%",
{"entities": [[44, 48, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Prognostizierte Ausschüttungsrandite* Mindestanlage Mitgliedschaft Im Anlagesusschuss Ankaufs- / Verkaufs- / Verkaufs(Teflimmobilfe)- / Baumanagementgebahr (inkl. USt.) Parformanceabhängige Vergütung Einmalige Strukturierungsgebühr Laufzeit / Investtionszeltraum Ausschüttungsintervalle Deutsche Metropolregianen und umliegende Regionen mit Städten >50T Einwohner Artikel 8 Wohnimmobilien Deutschland Aktive Bestandsentwicklung Offener Spezial-AlF mit festen Anlagebedingungen rd. 200 Mio. € / max. 20% rd. 250 Mio. € 7,5 % (nach Kosten & Gebühren, vor Steuern) 8 4,0 % {nach Kosten & Gebühren, var Steuern}",
{"entities": [[570, 575, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"5,00-5,25 % Ausschüttungsrendite",
{"entities": [[0, 11, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Zielrendite 5,00-5,25 % Ausschüttungsrendite",
{"entities": [[12, 23, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite 4,9% 5,3%",
{"entities": [[21, 25, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite 4,9% 5,3%",
{"entities": [[26, 30, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschittungsrendite 3,8% 5,7%",
{"entities": [[20, 24, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschittungsrendite 3,8% 5,7%",
{"entities": [[25, 29, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite 4,5% 4,6%",
{"entities": [[21, 25, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite 4,5% 4,6%",
{"entities": [[26, 30, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite 5,0% 4,7%",
{"entities": [[26, 30, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite 5,0% 4,7%",
{"entities": [[21, 25, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite “eons a Nuremberg aha 5,0 % 4,8 %",
{"entities": [[43, 48, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite “eons a Nuremberg aha 5,0 % 4,8 %",
{"entities": [[49, 54, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"3-4% dividend yield",
{"entities": [[0, 4, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Zielmärkte Klassifizierung SFDR Invastitionsfokus Rendite- / Risikoprofil Rechtsform Eigenkapital /FK Quote Investftionsvolumen Prognostizierte Gesamtrendite {IRR)* Prognostizierte Ausschüttungsrandite* Mindestanlage Mitgliedschaft Im Anlagesusschuss Ankaufs- / Verkaufs- / Verkaufs(Teflimmobilfe)- / Baumanagementgebahr (inkl. USt.) Parformanceabhängige Vergütung Einmalige Strukturierungsgebühr Deutsche Metropolregianen und umliegende Regionen mit Städten >50T Einwohner Artikel 8 Wohnimmobilien Deutschland Aktive Bestandsentwicklung Offener Spezial-AlF mit festen Anlagebedingungen rd. 200 Mio. € / max. 20% rd. 250 Mio. € 7,5 % (nach Kosten & Gebühren, vor Steuern) 8 4,0 % {nach Kosten & Gebühren, var Steuern} 5Mio.€ Ab 10 Mio. € 1,40 % / 0,80 % /2,12% / 4,91 % Laufzeit / Investtionszeltraum Ausschüttungsintervalle 20 % über einer @ Ausschüttungsrendite von 4,0 % 0,1% der bis zum 31.12.2023 erfolgten Kapitalzusagen (max. 200.000 &) 10 bis 12 Jahre / bis zu 24 Monate angestrebt Mindestens jährlich",
{"entities": [[945, 960, "LAUFZEIT"]]},
),
(
"Laufzeit / Investtionszeltraum,10 bis 12 Jahre / bis zu 24 Monate angestrebt Ausschüttungsintervalle,Mindestens jährlich",
{"entities": [[31, 46, "LAUFZEIT"]]},
),
(
"10-12 Jahre Laufzeit bei einem LTV von bis zu 20%",
{"entities": [[0, 11, "LAUFZEIT"]]},
),
(
"vom Manager festgelegter Stil Rechtsform Jahr des ersten Closings Laufzeit Geplantes Jahr der Auflösung Ziel-Netto-IRR / Gesamtrendite* Zielvolumen des Anlagevehikels Ziel-LTYAktueller LTV Zielsektoren Zielanlagestrategie Fonds Offen Deutschland Core, Core + Offener Immobilien-Spezialfonds 2022 10 - 12 Jahre",
{"entities": [[297, 310, "LAUFZEIT"], [247, 259, "RISIKOPROFIL"]]},
),
(
"Allgemeine Annahmen Ankaufsphase Haltedauer Zielobjektgröße Finanzierung Investitions-annahmen Zielrendite 24 Monate Investmentzeitraum 10 Jahre (+) EUR 20-75 Mio. Keine externe Finanzierung zum Auftakt (ausschließlich Darlehen der Anteilseigner). Die Finanzierung wird nach der Ankaufsphase und Stabilisierung der Zinssätze neu geprüft. Angestrebter LTV zwischen 25-40 % Investitionen für Renovierungen und ESG- Verbesserungen werden für jedes Objekt einzeln festgelegt. 5,00-5,25 % Ausschüttungsrendites",
{"entities": [[136, 148, "LAUFZEIT"], [472, 483, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Zielrendite 5,00-5,25 % Ausschüttungsrendite 1) Ankauf von Objekten an Tag eins mit 100% Eigenkapital. Die Strategie unterstellt die Aufnahme von Fremdkapital, sobald sich die Zins- und Finanzierungskonditionen nachhaltig stabilisieren. Strategie - Übersicht Risikoprofil Core+",
{"entities": [[12, 23, "AUSSCHÜTTUNGSRENDITE"], [272, 277, "RISIKOPROFIL"]]},
),
(
"Vehicle lifetime / investment period Open-ended fund",
{"entities": [[37, 52, "LAUFZEIT"]]},
),
(
"Vehicle / domicile Alternative Investment Fund / Luxembourg (e.g. SCSp SICAV-RAIF) Investment strategy eturn pro Real Estate (PropCo + OpCo) Investing in upscale hotels with long-term management contracts in major European destinations Core/Core+ with OpCo premium Management Agreements solely with financially strong and experienced partners/ global brands Cash flow-oriented Cash-flow pattern Target equity /AuM € 400m equity / € 800m AuM (50% Loan-to-Value) Vehicle lifetime / investment period Open-ended fund",
{"entities": [[498, 513, "LAUFZEIT"], [236, 245, "RISIKOPROFIL"]]},
),
(
"Vehicle type (Lux-RAIF) (net of fees) IRR6.5% ACCOR Vehicle structure Open-ended Targetvehiclesize € 400m (equity) Manager-defined Core/Core+ with | style OpCo Premium darge CLV. 50% Pt H | LTO N WORLDWIDE Year of first closing 2020 Target no. ofinvestors 1-5 Fund life (yrs} Open-ended Min-commitmentper —¢ 400m",
{"entities": [[131, 141, "RISIKOPROFIL"], [70, 80, "LAUFZEIT"]]},
),
(
"Fund term: Open-ended",
{"entities": [[11, 21, "LAUFZEIT"]]},
),
(
"Abdeckung der Risiko-Rendite-Bandbreite (Core, Core+, Value-Add)",
{"entities": [[41, 63, "RISIKOPROFIL"]]},
),
(
"5,1% - 8,5% IRR!",
{"entities": [[0, 11, "RENDITE"]]},
),
(
"Retailinvestitionsvolumen nach Ländern (2024) Vereinigtes Königreich, 26,4% Deutschland, 19,0% Andere, 19,7% Italien, 8,2% Irland, 3,3% N | Frankreich, Spanien, 8,1%",
{"entities": [[46, 75, "LÄNDERALLOKATION"], [76, 94, "LÄNDERALLOKATION"], [95, 108, "LÄNDERALLOKATION"], [109, 122, "LÄNDERALLOKATION"], [123, 135, "LÄNDERALLOKATION"]]},
),
(
"Erwartete IRR 5 (je nach Objekt- A(E) 6.00% - 8,00%",
{"entities": [[39, 52, "RENDITE"]]},
),
(
"Zielmarkts Deutsche Metropolregianen und umliegende Regionen mit Städten >50T Einwohner Klassifizierung SFDR Artikel 8 Invastitionsfokus Wohnimmobilien Deutschland Rendite- / Risikoprofil Aktive Bestandsentwicklung Rechtsform Offener Spezial-AlF mit festen Anlagebedingungen Eigenkapital /FK Quote rd. 200 Mio. € / max. 20% Investftionsvolumen rd. 250 Mio. € Prognostiderte Gesamtrendite {IRR)* 7,5 % (nach Kosten & Gebühren, vor Steuern) Prognostizierte Ausschüttungsrandite* @ 4,0 % {nach Kosten & Gebühren, var Steuern} Mindestanlage 5Mio.€ Mitgliedschaft Im Anlagesusschuss Ab 10 Mio. € Ankaufs- / Verkaufs- / Verkaufs(Teflimmobilfe)- / Baumanagementgebahr (inkl. USt) 1,40 %/080%/212%/491% Parformanceabhängige Vergütung 20 % über einer ® Ausschüttungsrendite von 4,0% Einmalige Strukturierungsgebühr 0,1% der bis zum 31.12.2023 erfolgten Kapitalzusagen (max. 200.000 €) Laufzelt / Investtonszeltraum 10 bis 12 Jahre / bis zu 24 Monate angestrebt Ausschüttungsintervalle Mindestens jährlich",
{"entities": [[396, 401, "RENDITE"], [482, 487, "AUSSCHÜTTUNGSRENDITE"], [914, 929, "LAUFZEIT"]]},
),
(
"= Prognostizierte jährliche Ausschüttung von @ 4,0%* = Prognostizierte Gesamtrendite (IRR) von 7,5%*",
{"entities": [[48, 52, "AUSSCHÜTTUNGSRENDITE"], [96, 100, "RENDITE"]]},
),
(
"Prognose: 7,5%+ IRR auf Fondsebene",
{"entities": [[10, 14, "RENDITE"]]},
),
(
"= Prognostizierte jährliche Ausschüttung* von 84,0% = Prognostizierte Gesamtrendite (IRR}* von 7,5%",
{"entities": [[96, 100, "RENDITE"], [49, 53, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"= Lagefokussierung: Metropolregionen Deutschlands = Finanzierung: max. 20% LTV = Risikoprofil: Core, Core +",
{"entities": [[95, 107, "RISIKOPROFIL"]]},
),
(
"Performance-Fee: 20% über einer @ Ausschüttungsrendite von 4,0%",
{"entities": [[61, 65, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Fondstyp Offener Spezial-AIF nach KAGB mit festen Anlagebedingungen ESG-Klassifizierung Fonds gemäß Artikel 8 EU-Offenlegungsverordnung KVG IntReal GmbH, Hamburg Anlagestrategie Aufbau eines Objektportfolios aus Ärztehäusern, die langfristig vermietet sind Ärztehäuser, Laborimmobilien, im Verbund mit Ärztehäusern auch ambulant Zielobjekte betreute Wohngemeinschaften; Mietanteil Medizin und medizinnahe Dienstleistungen/Handel > 65 % (Objektebene) WALT >5 Jahre bei Ankauf Objektbaujahre Ab 2000 Anlagegrenzen Einzelinvestment 8-30 Mio. EUR Anzahl Objekte 10-20 Deutschland bundesweit; jeweiliges Einzugsgebiet > 25.000 Einwohner mit Regionen stabiler Bevölkerungsprognose Risikoprofil Core / Core +",
{"entities": [[689, 702, "RISIKOPROFIL"]]},
),
(
"Fondsvolumen 300 Mio. EUR Zielrendite (IRR) > 6,0 % p. a. Ausschuttung >5,0 % p. a. Ankaufszeitraum 2024-2026 Laufzeit 31.12.2036 Mindestanlage 10 Mio. EUR Anlageausschuss Ja, entscheidet u. a. über Objekterwerb (Mitglied kann ab 20 Mio. EUR gestellt werden) Gebührenstruktur Marktüblich (auf Anfrage) Projektentwicklungen keine Forward-Deals Möglich, maximal 18 Monate Vorlauf; keine Projektentwicklungsrisiken beim Fonds Erbbaurechte Möglich, sofern Laufzeit > 60 Jahre und angemessene Entschädigung bei Ablauf und Heimfall Status Objektpipeline vorhanden: siehe Folie 16 ff.",
{"entities": [[44, 57, "RENDITE"], [71, 83, "AUSSCHÜTTUNGSRENDITE"], [120, 130, "LAUFZEIT"]]},
),
(
"Niederlande (max. 35 %) Länderallokation Frankreich (max. 35 %) (in % vom Zielvolumen) Skandinavien (Schweden, Dänemark) (max. 35 %) Deutschland (<= 10 %)",
{"entities": [[0, 23, "LÄNDERALLOKATION"], [41, 63, "LÄNDERALLOKATION"], [87, 132, "LÄNDERALLOKATION"], [133, 154, "LÄNDERALLOKATION"]]},
),
(
"Führender Immobilien-Investmentmanager in den Nordics für globale ll institutionelle Investoren in Value Add und Core Strategien",
{"entities": [[101, 119, "RISIKOPROFIL"]]},
),
(
"Core und Core+ Fonds",
{"entities": [[0, 14, "RISIKOPROFIL"]]},
),
(
"Risikoprofil Core / Core+",
{"entities": [[13, 25, "RISIKOPROFIL"]]},
),
(
"Durchschnittlich geplante jährliche Ausschüttung von 4,5-5,5% auf das investierte Eigenkapital an die Anleger Geplante Gesamtrendite von 5-6% (IRR) auf das eingezahlte Eigenkapital",
{"entities": [[54, 62, "AUSSCHÜTTUNGSRENDITE"], [138, 142, "RENDITE"]]},
),
(
"Geografische Zielallokation nach Investitionsphase des Fonds: 1) Schweden 20-60% Allokation Länder 2) Finnland 20-60% 3) Norwegen 10-40% 4) Dänemark 10-40%",
{"entities": [[65, 80, "LÄNDERALLOKATION"], [102, 117, "LÄNDERALLOKATION"], [121, 136, "LÄNDERALLOKATION"], [140, 155, "LÄNDERALLOKATION"]]},
),
(
"Deutsches Spezial-Sondervermögen mit festen Anlagebedingungen ($284 KAGB) Immobilien- oder Infrastrukturquote (nach Solvency II) Core / Core+ Euro Hauptstadtregionen und andere Großstädte in den Nordics €500 Mio. 4,5-5,5% 15 Jahre; Fonds hat unbegrenzte Laufzeit; Investmentphase 4 Jahre Maximaler Fremdkapitalanteil 50% (LTV-Ziel bei Ankauf), Langfristiges LTV-Ziel auf Fondsebene ist 45% 0,625% p. a. des Bruttofondsvermögens Zeichnungen ab € 30 Mio. - 0,03 % Rabatt Zeichnungen ab € 50 Mio. - zusatzl. 0,03 % Rabatt 1,1% des Verkehrswertes 0,6% der Bruttoverkaufswert 10% wenn Hurdle Rate 5,0 % p. a. (IRR netto) überschritten wird (nach 15 Jahren berechnet) Ja",
{"entities": [[129, 141, "RISIKOPROFIL"], [213, 221, "ZIELRENDITE"], [242, 262, "LAUFZEIT"]]},
),
(
"Standort Helsinki, Finnland Sektor Bildungswesen, Schule& Kindertagesstätte Vermietbare Fläche 3.321 m? Leerstand bei Ankauf 0% / 0% Ankaufspreis+ Investitionen €21,4 Mio. + €0,2 Mio Eigenkapital €21,6 Mio. Ankaufs- / Stabilisierungs- / Exitrendite 5,0%/ 5,5%/ 5,0% NOI zum Ankaufszeitpunkt / Exit-NOI €1.1m/ €1.2m Zielrenditen (netto für LPs) 5,4% IRR/ 1.5x EM / DY 4,3% Ankauf / Exit Dezember 2023/ Dezember 2033",
{"entities": [[345, 349, "ZIELRENDITE"]]},
),
(
"Evergreen/offene Fondsstrukturenv Core / Core+ Strategien",
{"entities": [[34, 46, "RISIKOPROFIL"]]},
),
(
"BEE Henderson German 2012 Logistik Core/D/Art. 8 € 336 Mio. 12 (voll investiert) 13,0 % p.a.",
{"entities": [[35, 39, "RISIKOPROFIL"], [81, 87, "RENDITE"]]},
),
(
"ICF German Logistics 2014 Logistik Core/D/Art. 8 € 400 Mio. 16 (voll investiert) 12,0 % p.a.",
{"entities": [[35, 39, "RISIKOPROFIL"], [81, 87, "RENDITE"]]},
),
(
"Individualmandat 2015 Logistik Core / D+AU/ ArTt. 6 € 200 Mio. 8 (realisiert) 8,0 % p.a.",
{"entities": [[31, 35, "RISIKOPROFIL"], [78, 83, "RENDITE"]]},
),
(
"European Logistics Partnership” 2017 Logistik Value-Add / Europ/a - € 1.000 Mio. 28 (realisiert) 20,0 % p.a.",
{"entities": [[46, 55, "RISIKOPROFIL"], [97, 103, "RENDITE"]]},
),
(
"European Core Logistics Fund (ECLF 1) 2021 Logistik Core / Euro/p Arat. 8 € 314 Mio. 12 (voll investiert) 7,50 % p.a.",
{"entities": [[9, 13, "RISIKOPROFIL"], [106, 112, "RENDITE"]]},
),
(
"P-Logistik Europa Fonds (ECLF 2) 2022 Logistik Core / Euro/p Arat. 8 € 150 Mio.? A (voll investiert) 6,5 % p.a.?",
{"entities": [[47, 51, "RISIKOPROFIL"], [101, 106, "RENDITE"]]},
),
(
"First Business Parks 2015 Light Industrial Value Add / D+AUT € 100 Mio. 6 (realisiert) 16,0 % p.a.",
{"entities": [[43, 52, "RISIKOPROFIL"], [87, 93, "RENDITE"]]},
),
(
"Unternehmensimmobilien Club 1 2016 Light Industrial Core+/D € 186 Mio. 9 (voll investiert) 13,0 % p.a.",
{"entities": [[91, 97, "RENDITE"]]},
),
(
"Unternehmensimmobilien Club 1 2016 Light Industrial Core+/D € 186 Mio. 9 (voll investiert) 13,0 % p.a.",
{"entities": [[52, 57, "RISIKOPROFIL"], [91, 97, "RENDITE"]]},
),
(
"Unternehmensimmobilien Club 2 2021 Light Industrial Core+/D € 262 Mio. 12 (voll investiert) 9,00 % p.a.",
{"entities": [[52, 57, "RISIKOPROFIL"], [92, 98, "RENDITE"]]},
),
(
"Individualmandat 2022 Light Industrial Value-Add / Nordics € 100 Mio. 5 (voll investiert) 18,0 % p.a.",
{"entities": [[39, 48, "RISIKOPROFIL"], [90, 96, "RENDITE"]]},
),
(
"EUROPEAN CORE LOGISTICS FUND 3",
{"entities": [[9, 13, "RISIKOPROFIL"]]},
),
(
"Core Investitionen",
{"entities": [[0, 4, "RISIKOPROFIL"]]},
),
(
"8 % IRR",
{"entities": [[0, 3, "RENDITE"]]},
),
(
"Rendite-Risiko-Profil Core ° Geographischer Fokus Kontinentaleuropaische Kernvolkswirtschaften nach Allokationsprofil * Sektoraler Fokus Logistikimmobilien nach Allokationsprofil Kapitalstruktur ° Eigenkapital € 250 Mio. ° Fremdkapital 50 % angestrebt, max. 60 % der Immobilienwerte (Objektebene) °e Mindestzeichnung € 10 Mio. Vehikelstruktur ° Rechtsform Immobilien-Spezial-AlF mit festen Anlagebedingungen nach 3 284 KAGB ° Klassifikation Artikel 8 Offenlegungsverordnung ¢ Anlagehorizont 10 Jahre mit Verlängerungsoption um 2 Jahre! ° Geplante Auflage 01 2025 Performanceziel? ° Ausschüttung 6,0 % p.a. (Durchschnitt 10 Jahre Haltedauer) ° Interner Zinsfuß (IRR) 8,0 % p.a. (10 Jahre Haltedauer, Target-IRR)",
{"entities": [[22, 26, "RISIKOPROFIL"], [596, 601, "AUSSCHÜTTUNGSRENDITE"], [667, 672, "RENDITE"]]},
),
(
"Core/Core+, mit Cash-Flow-Stabilität",
{"entities": [[0, 10, "RISIKOPROFIL"]]},
),
(
"Zielausschüttung: min. 5,10%",
{"entities": [[24, 29, "ZIELAUSSCHÜTTUNG"]]},
),
(
"Zielrendite (IRR): min. 5,50%",
{"entities": [[24, 29, "ZIELRENDITE"]]},
),
(
"Rewe & Lidl Maxhütte-Haidhof é ae: 6 s Bahnhof Ankermieter REWE & Lidl er WALT 20 und 17 Jahre Miete p.a. 1.127.916 € Kaufpreis 21,43 Mio. € Faktor 19,00 x LTV / Zins 80% / 4,0% Ausschüttung 5,7 % IRR 7,1%",
{"entities": [[193, 198, "AUSSCHÜTTUNGSRENDITE"], [203, 207, "ZIELRENDITE"]]},
),
(
"Real Estate Prime Europe Access the Core of European Prime Cities with a green SRI fund including a genuine low carbon commitment",
{"entities": [[36, 40, "RISIKOPROFIL"]]},
),
(
"(FR, UK, DE, BE, NL, LU, Nordics, Allocation SP, IT, CH)",
{"entities": [[1, 32, "LÄNDERALLOKATION"], [45, 55, "LÄNDERALLOKATION"]]},
),
(
"IRR: 6% - 7%",
{"entities": [[5, 12, "RENDITE"]]},
),
(
"Europe | Germany 67 Value Add",
{"entities": [[9, 16, "LÄNDERALLOKATION"], [20, 29, "RISIKOPROFIL"]]},
),
(
"Germany, Norway 336 Core Plus",
{"entities": [[0, 7, "LÄNDERALLOKATION"], [20, 29, "RISIKOPROFIL"]]},
),
(
"UK",
{"entities": [[0, 2, "LÄNDERALLOKATION"]]},
),
(
"NORWAY",
{"entities": [[0, 6, "LÄNDERALLOKATION"]]},
),
(
"9.8% IRR",
{"entities": [[0, 4, "RENDITE"]]},
),
(
"Investment volume down 52% to €2.3 billion, with 4,000 100 14% value-add and core-plus increasing YoY",
{"entities": [[63, 86, "RISIKOPROFIL"]]},
),
(
"Geared Gross IRR seeking a range of 16-18% per annum",
{"entities": [[37, 43, "RENDITE"]]},
),
(
"Open-ended fund 24 months, incl. rolling reinvestment Sale of individual assets with respective management contracts or geared leases IRR: >6.5% | CoC: >5.0%",
{"entities": [[0, 10, "LAUFZEIT"], [139, 144, "RENDITE"]]},
),
(
"Our investment strategy focuses on investing in upscale hotels in European prime locations, including DACH, Italy, Spain, Portugal, France, UK, Denmark, Benelux,and Poland.",
{"entities": [[102, 171, "LÄNDERALLOKATION"]]},
),
(
"Core+ assets with value-add potential, Emerging Gateway Cities Helsinki] Core+ with Value well-mitigated risk and great upside Potential potential through asset improvement or = Max. 20% UK & Ireland {no contract renegotiation currency risk hedging], 80% tinental E > IRR target of 6-9%",
{"entities": [[0, 5, "RISIKOPROFIL"], [282, 286, "RENDITE"]]},
),
(
"10% net IRR since inception in 2018?",
{"entities": [[0, 3, "RENDITE"]]},
),
(
"Eurozone: Benelux, France and Germany",
{"entities": [[10, 37, "LÄNDERALLOKATION"]]},
),
(
"Open-ended, with quarterly liquidity (redemption rights, dual pricing)",
{"entities": [[0, 10, "LAUFZEIT"]]},
),
(
"Class A & B (Institutional): 0.93% on NAV; Class D (Wholesale): 1.80% on NAV; Class P (Wholesale): 1.25% on NAV",
{"entities": [[29, 34, "MANAGMENTGEBÜHREN"], [64, 69, "MANAGMENTGEBÜHREN"], [99, 104, "MANAGMENTGEBÜHREN"]]},
),
(
"Risk profile: favour core > © at least and core+ assets with a targeted N 2 n allocation to value add assets to enhance returns",
{"entities": [[21, 25, "RISIKOPROFIL"], [43, 48, "RISIKOPROFIL"]]},
),
(
"The Netherlands (38 assets) = Germany (9 assets) 10 largest Country assets split France (8 assets)",
{"entities": [[0, 15, "LÄNDERALLOKATION"], [30, 37, "LÄNDERALLOKATION"], [81, 87, "LÄNDERALLOKATION"]]},
),
(
"Expected IRR 10.9%",
{"entities": [[13, 18, "ZIELRENDITE"]]},
),
(
"Structure Open-end, perpetual life, Luxembourg domiciled Initial Target Size* €2 billion 6-8% total return,",
{"entities": [[10, 18, "LAUFZEIT"], [89, 93, "RENDITE"]]},
),
(
"Geographic Focus: UK, Ireland, Iberia, Nordics, Netherlands, Germany, France, Italy",
{"entities": [[18, 83, "LÄNDERALLOKATION"]]},
),
(
"IRR of 13-14%",
{"entities": [[7, 13, "RENDITE"]]},
),
(
"Value-add",
{"entities": [[0, 9, "RISIKOPROFIL"]]},
),
(
"Geographic allocation NORDICS UNITED KINGDOM GERMANY FRANCE PORTUGAL BENELUX",
{"entities": [[22, 76, "LÄNDERALLOKATION"]]},
),
(
"Strong track record delivering a 17% net IRR, 1.7x net multiple across all divested assets (both discretionary and non-discretionary mandates)",
{"entities": [[33, 36, "RENDITE"]]},
),
(
"Targeting a 7-8% net annual return and a 3-4% dividend yield, reflecting a target LTV of 35% (capped at 37.5%)",
{"entities": [[12, 16, "RENDITE"]]},
),
(
"Sweden Norway Denmark Finland",
{"entities": [[0, 29, "LÄNDERALLOKATION"]]},
),
(
"Logistics Residential Office Other",
{"entities": [[0, 34, "SEKTORENALLOKATION"]]},
),
(
"Fund Term Open-ended with an initial 24-month lock-in for new investors",
{"entities": [[10, 20, "LAUFZEIT"]]},
),
(
"Management fee of 85 bps on NAV.",
{"entities": [[18, 24, "MANAGMENTGEBÜHREN"]]},
),
(
"Core/Core+ strategy, with tactical exposure to development projects aiming at enhancing the quality of the portfolio over time",
{"entities": [[0, 10, "RISIKOPROFIL"]]},
),
(
"Fund term: Open-ended",
{"entities": [[11, 21, "LAUFZEIT"]]},
),
(
"Return targets: The fund targets a net internal rate of return (IRR) of 8% and a net annual income yield of 5% with planned quarterly distributions.",
{"entities": [[72, 74, "RENDITE"]]},
),
(
"Geographic scope: The fund has a broad mandate to invest in commercial and residential real estate across Sweden, Denmark, Finland, and Norway. 50% LTV Asset selection: Heirs to acquire high-quality, income-generating properties in major Nordic cities and enhance their value through active asset management. Portfolio construction: The goal is to build diversified portfolios that are appealing to core buyers upon exit.",
{"entities": [[106, 142, "LÄNDERALLOKATION"]]},
),
(
"Experience: Since 2012, | | has demonstrated its capability to build diversified and resilient portfolios for its core-plus funds. German Real Estate Quota advantage . Local expertise: extensive local relationships and proprietary deal flow in key Nordic markets provide a strategic advantage.",
{"entities": [[114, 123, "RISIKOPROFIL"]]},
),
(
"Target returns: 8% net IRR with 5% net annual income yield! * Geographic focus: Sweden, Denmark, Norway and Finland « Target leverage: 50% LTV (excluding short-term borrowing) « Sector exposure: office, logistics, public properties, retail (focused on grocery anchored and necessity driven retail) and residentials « Investment focus: high quality properties,",
{"entities": [[16, 18, "RENDITE"], [80, 115, "LÄNDERALLOKATION"], [195, 239, "SEKTORENALLOKATION"]]},
),
(
"The Fund 2 xemoours common limited partnership (SCS) (SICAV-RAIF) Investment Objective To pursue investments in commercial and residential properties throughout the Nordic Region Fund Target Size €300 million (equity) Return Targets Target net IRR of 8%, target net annual income yield of 5%",
{"entities": [[251, 253, "RENDITE"]]},
)
]

View File

@ -1,40 +0,0 @@
import os
from pathlib import Path
import spacy
from spacy.cli.train import train
from spacy.tokens import DocBin
from tqdm import tqdm
from training_data import TRAINING_DATA
nlp = spacy.blank("de")
# create a DocBin object
db = DocBin()
for text, annot in tqdm(TRAINING_DATA):
doc = nlp.make_doc(text)
ents = []
# add character indexes
for start, end, label in annot["entities"]:
span = doc.char_span(start, end, label=label, alignment_mode="contract")
if span is None:
print(f"Skipping entity: |{text[start:end]}| Start: {start}, End: {end}, Label: {label}")
else:
ents.append(span)
# label the text with the ents
doc.ents = ents
db.add(doc)
# save the DocBin object
os.makedirs("./data", exist_ok=True)
db.to_disk("./data/train.spacy")
config_path = Path("config.cfg")
output_path = Path("output")
print("Starte Training...")
train(config_path, output_path)

View File

@ -0,0 +1 @@
{"running": false}

View File

@ -40,7 +40,9 @@ def send_to_coordinator_service(processed_data, request_id):
def process_data_async(request_id, spacy_data, exxeta_data):
try:
requests.post(COORDINATOR_URL + "/api/progress", json={"id": request_id, "progress": 95})
requests.post(
COORDINATOR_URL + "/api/progress", json={"id": request_id, "progress": 95}
)
print(f"Start asynchronous processing for PitchBook: {request_id}")
# Perform merge
@ -96,7 +98,6 @@ def validate():
# If both datasets are present, start asynchronous processing
if spacy_data is not None and exxeta_data is not None:
# Start asynchronous processing in a separate thread
processing_thread = threading.Thread(
target=process_data_async,

View File

@ -27,7 +27,6 @@ def merge_entities(spacy_data, exxeta_data):
and s_entity_norm == e_entity_norm
and s_page == e_page
):
merged.append(
{
"label": s["label"],

View File

@ -5,6 +5,8 @@ import os
# SETTINGS = [{"id": "Rendite", "type": "number"}]
COORDINATOR_URL = os.getenv("COORDINATOR_URL", "http://localhost:5000")
def validate_entities(entities):
try:
response = requests.get(COORDINATOR_URL + "/api/kpi_setting/")
@ -42,7 +44,6 @@ def validate_entities(entities):
result.extend(item[1])
continue
# Filter not validated, if there are valid values
validated = False
for entity in item[1]:
@ -61,11 +62,11 @@ def validate_entities(entities):
def validate_number(entity_list, settings):
filtered_kpi = {}
for label, entity_list in entity_list.items():
setting = next((s for s in settings if s["name"].upper() == label), None)
if setting and setting["type"] == "number":
filtered_entities = [
entity for entity in entity_list
entity
for entity in entity_list
if is_valid_number(str(entity["entity"]))
]
for entity in entity_list:
@ -80,8 +81,12 @@ def validate_number(entity_list, settings):
def is_valid_number(number):
pattern = r'^[0-9\-\s%,.€]+$'
return any(char.isdigit() for char in number) and not re.search(r'\d+\s\d+', number) and re.fullmatch(pattern, number)
pattern = r"^[0-9\-\s%,.€]+$"
return (
any(char.isdigit() for char in number)
and not re.search(r"\d+\s\d+", number)
and re.fullmatch(pattern, number)
)
def delete_exxeta_unknown(entity_list):
@ -89,11 +94,16 @@ def delete_exxeta_unknown(entity_list):
for label, entity_list in entity_list.items():
# Filter out entities with "nichtangegeben" or "n/a" (case-insensitive and stripped)
filtered_entities = [
entity for entity in entity_list
if str(entity["entity"]).lower().replace(" ", "") not in {"nichtangegeben", "n/a"}
entity
for entity in entity_list
if str(entity["entity"]).lower().replace(" ", "")
not in {"nichtangegeben", "n/a"}
]
for entity in entity_list:
if str(entity["entity"]).lower().replace(" ", "") in {"nichtangegeben", "n/a"}:
if str(entity["entity"]).lower().replace(" ", "") in {
"nichtangegeben",
"n/a",
}:
print(f"filtered out: {entity}")
if filtered_entities: # Only add the label if there are entities left
filtered_kpi[label] = filtered_entities
@ -115,6 +125,7 @@ def delete_duplicate_entities(entity_list):
unique_entities[label] = filtered_entities
return unique_entities
if __name__ == "__main__":
entities = [
# {"label": "PERSON", "entity": "John Doe", "status": "validated"},

View File

@ -58,6 +58,8 @@ services:
- VALIDATE_SERVICE_URL=http://validate:5000/validate
ports:
- 5052:5052
volumes:
- ./backend/spacy-service/spacy_training:/app/spacy_training
exxeta:
build:

View File

@ -4,7 +4,9 @@ WORKDIR /usr/src/app
# install dependencies into temp directory
# this will cache them and speed up future builds
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
#RUN bun install --frozen-lockfile
RUN bun install
COPY . .

6335
project/frontend/package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@
"@biomejs/biome": "1.9.4",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/file-saver": "^2.0.7",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",

View File

@ -6,41 +6,56 @@ import type { Kennzahl } from "../types/kpi";
import { getDisplayType } from "../types/kpi";
import { fetchKennzahlen as fetchK } from "../util/api";
import { API_HOST } from "../util/api";
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
type ConfigTableProps = {
export type ConfigTableProps = {
from?: string;
trainingRunning?: boolean;
};
export function ConfigTable({ from }: ConfigTableProps) {
export function ConfigTable({ from, trainingRunning }: ConfigTableProps) {
const navigate = useNavigate();
const [kennzahlen, setKennzahlen] = useState<Kennzahl[]>([]);
const [draggedItem, setDraggedItem] = useState<Kennzahl | null>(null);
const [isUpdatingPositions, setIsUpdatingPositions] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchKennzahlen = async () => {
while (true) {
try {
console.log("Fetching kennzahlen from API...");
const data = await fetchK();
console.log("Fetched kennzahlen:", data);
const sortedData = data.sort(
(a: Kennzahl, b: Kennzahl) => a.position - b.position,
);
setKennzahlen(sortedData);
setLoading(false);
break;
} catch (err) {
console.error("Error fetching kennzahlen:", err);
await new Promise((resolve) => setTimeout(resolve, 2000));
}
const fetchKennzahlen = async () => {
while (true) {
try {
console.log("Fetching kennzahlen from API...");
const data = await fetchK();
console.log("Fetched kennzahlen:", data);
const sortedData = data.sort((a, b) => a.position - b.position);
setKennzahlen(sortedData);
setLoading(false);
break;
} catch (err) {
console.error("Error fetching kennzahlen:", err);
await new Promise((resolve) => setTimeout(resolve, 2000));
}
};
}
};
useEffect(() => {
fetchKennzahlen();
}, []);
useEffect(() => {
if (trainingRunning === false) {
console.log("[ConfigTable] Training beendet → Kennzahlen neu laden...");
fetchKennzahlen();
}
}, [trainingRunning]);
const handleToggleActive = async (id: number) => {
const kennzahl = kennzahlen.find((k) => k.id === id);
if (!kennzahl) return;
@ -326,15 +341,32 @@ export function ConfigTable({ from }: ConfigTableProps) {
padding: "12px",
fontSize: "14px",
color: "#333",
display: "flex",
alignItems: "center",
gap: "8px",
}}
>
<span title={`Click to view details (ID: ${kennzahl.id})`}>
{kennzahl.name}
{kennzahl.mandatory && (
<span> *</span>
)}
{kennzahl.mandatory && <span> *</span>}
</span>
{kennzahl.is_trained === false && (
<Tooltip
title={
<>
<b>Diese Kennzahl ist nicht trainiert.</b><br />
Klicken Sie oben auf <i>"Neu trainieren"</i>, um das Training zu starten.
</>
}
arrow
placement="right"
>
<WarningAmberIcon sx={{ color: "#f57c00", fontSize: 20 }} />
</Tooltip>
)}
</td>
<td style={{ padding: "12px" }}>
<span
style={{

View File

@ -1,8 +1,13 @@
import { Box, Typography, Button, Paper, TextField, FormControlLabel,
Checkbox, Select, MenuItem, FormControl, InputLabel, Divider, CircularProgress } from "@mui/material";
import {
Box, Typography, Button, Paper, TextField, FormControlLabel,
Checkbox, Select, MenuItem, FormControl, InputLabel, Divider, CircularProgress
} from "@mui/material";
import { useState, useEffect } from "react";
import type { Kennzahl } from "../types/kpi";
import { typeDisplayMapping } from "../types/kpi";
import Snackbar from "@mui/material/Snackbar";
import MuiAlert from "@mui/material/Alert";
interface KPIFormProps {
mode: 'add' | 'edit';
@ -10,47 +15,138 @@ interface KPIFormProps {
onSave: (data: Partial<Kennzahl>) => Promise<void>;
onCancel: () => void;
loading?: boolean;
resetTrigger?: number;
}
const emptyKPI: Partial<Kennzahl> = {
name: '',
description: '',
mandatory: false,
type: 'string',
translation: '',
example: '',
active: true
active: true,
examples: [{ sentence: '', value: '' }],
};
export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }: KPIFormProps) {
export function KPIForm({ mode, initialData, onSave, onCancel, loading = false, resetTrigger }: KPIFormProps) {
const [formData, setFormData] = useState<Partial<Kennzahl>>(emptyKPI);
const [isSaving, setIsSaving] = useState(false);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState("");
const [snackbarSeverity, setSnackbarSeverity] = useState<'success' | 'error' | 'info'>("success");
useEffect(() => {
if (mode === 'edit' && initialData) {
setFormData(initialData);
} else {
} else if (mode === 'add') {
setFormData(emptyKPI);
}
}, [mode, initialData]);
useEffect(() => {
if (mode === 'add') {
setFormData(emptyKPI);
}
}, [resetTrigger]);
const handleSave = async () => {
if (!formData.name?.trim()) {
alert('Name ist erforderlich');
setSnackbarMessage("Name ist erforderlich");
setSnackbarSeverity("error");
setSnackbarOpen(true);
return;
}
if (!formData.examples || formData.examples.length === 0) {
setSnackbarMessage("Mindestens ein Beispielsatz ist erforderlich");
setSnackbarSeverity("error");
setSnackbarOpen(true);
return;
}
for (const ex of formData.examples) {
if (!ex.sentence?.trim() || !ex.value?.trim()) {
setSnackbarMessage('Alle Beispielsätze müssen vollständig sein.');
setSnackbarSeverity("error");
setSnackbarOpen(true);
return;
}
}
setIsSaving(true);
try {
await onSave(formData);
} catch (error) {
console.error('Error saving KPI:', error);
const spacyEntries = generateSpacyEntries(formData);
// Für jeden einzelnen Beispielsatz:
for (const entry of spacyEntries) {
// im localStorage speichern (zum Debuggen oder Vorschau)
const stored = localStorage.getItem("spacyData");
const existingData = stored ? JSON.parse(stored) : [];
const updated = [...existingData, entry];
localStorage.setItem("spacyData", JSON.stringify(updated));
// POST Request an das Flask-Backend
const response = await fetch("http://localhost:5050/api/spacy/append-training-entry", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(entry)
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "Fehler beim Aufruf von append-training-entry");
}
console.log("SpaCy-Eintrag gespeichert:", data);
}
// Dann in die DB speichern
await onSave({
name: formData.name!,
mandatory: formData.mandatory ?? false,
type: formData.type || 'string',
position: formData.position ?? 0,
active: formData.active ?? true,
examples: formData.examples ?? [],
is_trained: false,
});
// Formular zurücksetzen:
setFormData(emptyKPI);
setSnackbarMessage("Beispielsätze gespeichert. Jetzt auf -Neu trainieren- klicken oder weitere Kennzahlen hinzufügen.");
setSnackbarSeverity("success");
setSnackbarOpen(true);
} catch (e: any) {
// Prüfe auf 409-Fehler
if (e?.message?.includes("409") || e?.response?.status === 409) {
setSnackbarMessage("Diese Kennzahl existiert bereits. Sie können sie unter -Konfiguration- bearbeiten.");
setSnackbarSeverity("info");
setSnackbarOpen(true);
} else {
setSnackbarMessage(e.message || "Fehler beim Speichern.");
setSnackbarSeverity("error");
setSnackbarOpen(true);
}
console.error(e);
} finally {
setIsSaving(false);
}
};
const handleCancel = () => {
setFormData(emptyKPI);
onCancel();
};
@ -58,6 +154,24 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
setFormData(prev => ({ ...prev, [field]: value }));
};
const updateExample = (index: number, field: 'sentence' | 'value', value: string) => {
const newExamples = [...(formData.examples || [])];
newExamples[index][field] = value;
updateField('examples', newExamples);
};
const addExample = () => {
const newExamples = [...(formData.examples || []), { sentence: '', value: '' }];
updateField('examples', newExamples);
};
const removeExample = (index: number) => {
const newExamples = [...(formData.examples || [])];
newExamples.splice(index, 1);
updateField('examples', newExamples);
};
if (loading) {
return (
<Box
@ -76,172 +190,222 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
}
return (
<Paper
elevation={2}
sx={{
width: "90%",
maxWidth: 800,
p: 4,
borderRadius: 2,
backgroundColor: "white"
}}
>
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Kennzahl
</Typography>
<TextField
fullWidth
label="Name *"
value={formData.name || ''}
onChange={(e) => updateField('name', e.target.value)}
sx={{ mb: 2 }}
required
error={!formData.name?.trim()}
helperText={!formData.name?.trim() ? 'Name ist erforderlich' : ''}
/>
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Beschreibung
</Typography>
<TextField
fullWidth
multiline
rows={3}
label="Beschreibung"
value={formData.description || ''}
onChange={(e) => updateField('description', e.target.value)}
helperText="Beschreibung der Kennzahl"
/>
<Box mt={3}>
<FormControlLabel
control={
<Checkbox
checked={formData.mandatory || false}
onChange={(e) => updateField('mandatory', e.target.checked)}
sx={{ color: '#383838' }}
/>
}
label="Erforderlich"
<>
<Paper
elevation={2}
sx={{
width: "90%",
maxWidth: 800,
p: 4,
borderRadius: 2,
backgroundColor: "white"
}}
>
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Kennzahl
</Typography>
<TextField
fullWidth
label="Name *"
placeholder="z.B. IRR"
value={formData.name || ''}
onChange={(e) => updateField('name', e.target.value)}
sx={{ mb: 2 }}
required
error={!formData.name?.trim()}
helperText={!formData.name?.trim() ? 'Name ist erforderlich' : ''}
/>
<Typography variant="body2" color="text.secondary" ml={4}>
Die Kennzahl erlaubt keine leeren Werte
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Format: {typeDisplayMapping[formData.type as keyof typeof typeDisplayMapping] || formData.type}
</Typography>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Typ</InputLabel>
<Select
value={formData.type || 'string'}
label="Typ"
onChange={(e) => updateField('type', e.target.value)}
>
<MenuItem value="string">Text</MenuItem>
<MenuItem value="number">Zahl</MenuItem>
<MenuItem value="date">Datum</MenuItem>
<MenuItem value="boolean">Ja/Nein</MenuItem>
<MenuItem value="array">Liste (mehrfach)</MenuItem>
</Select>
</FormControl>
</Box>
{mode === 'add' && (
<>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<FormControlLabel
control={
<Checkbox
checked={formData.active !== false}
onChange={(e) => updateField('active', e.target.checked)}
sx={{ color: '#383838' }}
/>
}
label="Aktiv"
/>
<Typography variant="body2" color="text.secondary" ml={4}>
Die Kennzahl ist aktiv und wird angezeigt
</Typography>
</Box>
<Box mt={3}>
<FormControlLabel
control={
<Checkbox
checked={formData.mandatory || false}
onChange={(e) => updateField('mandatory', e.target.checked)}
sx={{ color: '#383838' }}
/>
}
label="Erforderlich"
/>
<Typography variant="body2" color="text.secondary" ml={4}>
Die Kennzahl erlaubt keine leeren Werte
</Typography>
</Box>
</>
)}
<Divider sx={{ my: 3 }} />
{/* Hinweistext vor Beispielsätzen */}
<Box mb={2} p={2} sx={{ backgroundColor: '#fff8e1', border: '1px solid #ffe082', borderRadius: 2 }}>
<Typography variant="body1" sx={{ fontWeight: 'bold', mb: 1 }}>
Hinweis zur Trainingsqualität
</Typography>
<Typography variant="body2">
Damit das System neue Kennzahlen zuverlässig erkennen kann, empfehlen wir <strong>mindestens 5 Beispielsätze</strong> zu erstellen je mehr, desto besser.
</Typography>
<Typography variant="body2" mt={1}>
<strong>Wichtig:</strong> Neue Kennzahlen werden erst in PDF-Dokumenten erkannt, wenn Sie den Button <em>"Neu trainieren"</em> auf der Konfigurationsseite ausführen.
</Typography>
<Typography variant="body2" mt={1}>
<strong>Tipp:</strong> Sie können jederzeit weitere Beispielsätze hinzufügen oder vorhandene in der Kennzahlenverwaltung bearbeiten.
</Typography>
</Box>
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Format: {typeDisplayMapping[formData.type as keyof typeof typeDisplayMapping] || formData.type}
</Typography>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Typ</InputLabel>
<Select
value={formData.type || 'string'}
label="Typ"
onChange={(e) => updateField('type', e.target.value)}
<Typography variant="h6" fontWeight="bold" mb={2}>
Beispielsätze
</Typography>
{(formData.examples || []).map((ex, idx) => (
<Box key={idx} sx={{ mb: 2, border: '1px solid #ccc', p: 2, borderRadius: 1 }}>
<TextField
fullWidth
multiline
label={`Beispielsatz ${idx + 1}`}
placeholder="z.B. Die IRR beträgt 7,8 %"
value={ex.sentence}
onChange={(e) => updateExample(idx, 'sentence', e.target.value)}
required
sx={{ mb: 1 }}
/>
<TextField
fullWidth
label="Bezeichneter Wert im Satz"
placeholder="z.B. 7,8 %"
value={ex.value}
onChange={(e) => updateExample(idx, 'value', e.target.value)}
required
/>
{(formData.examples?.length || 0) > 1 && (
<Button onClick={() => removeExample(idx)} sx={{ mt: 1 }} color="error">
Entfernen
</Button>
)}
</Box>
))}
<Button variant="outlined" onClick={addExample}>
+ Beispielsatz hinzufügen
</Button>
</Box>
<Box display="flex" justifyContent="flex-end" gap={2} mt={4}>
<Button
variant="outlined"
onClick={handleCancel}
disabled={isSaving}
sx={{
borderColor: "#383838",
color: "#383838",
"&:hover": { borderColor: "#2e2e2e", backgroundColor: "#f5f5f5" }
}}
>
<MenuItem value="string">Text</MenuItem>
<MenuItem value="number">Zahl</MenuItem>
<MenuItem value="date">Datum</MenuItem>
<MenuItem value="boolean">Ja/Nein</MenuItem>
<MenuItem value="array">Liste (mehrfach)</MenuItem>
</Select>
</FormControl>
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Synonyme & Übersetzungen
</Typography>
<TextField
fullWidth
label="Übersetzung"
value={formData.translation || ''}
onChange={(e) => updateField('translation', e.target.value)}
helperText="z.B. Englische Übersetzung der Kennzahl"
/>
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Beispiele von Kennzahl
</Typography>
<TextField
fullWidth
multiline
rows={2}
label="Beispiel"
value={formData.example || ''}
onChange={(e) => updateField('example', e.target.value)}
helperText="Beispielwerte für diese Kennzahl"
/>
</Box>
{mode === 'add' && (
<>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<FormControlLabel
control={
<Checkbox
checked={formData.active !== false}
onChange={(e) => updateField('active', e.target.checked)}
sx={{ color: '#383838' }}
/>
}
label="Aktiv"
/>
<Typography variant="body2" color="text.secondary" ml={4}>
Die Kennzahl ist aktiv und wird angezeigt
</Typography>
</Box>
</>
)}
<Box display="flex" justifyContent="flex-end" gap={2} mt={4}>
<Button
variant="outlined"
onClick={handleCancel}
disabled={isSaving}
sx={{
borderColor: "#383838",
color: "#383838",
"&:hover": { borderColor: "#2e2e2e", backgroundColor: "#f5f5f5" }
}}
Abbrechen
</Button>
<Button
variant="contained"
onClick={handleSave}
disabled={isSaving || !formData.name?.trim()}
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
>
{isSaving ? (
<>
<CircularProgress size={16} sx={{ mr: 1, color: 'white' }} />
{mode === 'add' ? 'Hinzufügen...' : 'Speichern...'}
</>
) : (
mode === 'add' ? 'Hinzufügen' : 'Speichern'
)}
</Button>
</Box>
</Paper>
<Snackbar
open={snackbarOpen}
autoHideDuration={5000}
onClose={() => setSnackbarOpen(false)}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<MuiAlert
elevation={6}
variant="filled"
onClose={() => setSnackbarOpen(false)}
severity={snackbarSeverity}
sx={{ width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
>
Abbrechen
</Button>
<Button
variant="contained"
onClick={handleSave}
disabled={isSaving || !formData.name?.trim()}
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
>
{isSaving ? (
<>
<CircularProgress size={16} sx={{ mr: 1, color: 'white' }} />
{mode === 'add' ? 'Hinzufügen...' : 'Speichern...'}
</>
) : (
mode === 'add' ? 'Hinzufügen' : 'Speichern'
)}
</Button>
</Box>
</Paper>
<span>{snackbarMessage}</span>
<Button color="inherit" size="small" onClick={() => setSnackbarOpen(false)}>
OK
</Button>
</MuiAlert>
</Snackbar>
</>
);
}
}
function generateSpacyEntries(formData: Partial<Kennzahl>) {
const label = formData.name?.trim().toUpperCase() || "";
return (formData.examples || []).map(({ sentence, value }) => {
const trimmedValue = value.trim();
const start = sentence.indexOf(trimmedValue);
if (start === -1) {
throw new Error(`"${trimmedValue}" nicht gefunden in Satz: "${sentence}"`);
}
return {
text: sentence,
entities: [[start, start + trimmedValue.length, label]]
};
});
}

View File

@ -5,6 +5,7 @@ import { KPIForm } from "../components/KPIForm";
import type { Kennzahl } from "../types/kpi";
import { API_HOST } from "../util/api";
export const Route = createFileRoute("/config-add")({
component: ConfigAddPage,
validateSearch: (search: Record<string, unknown>): { from?: string } => {
@ -47,19 +48,28 @@ function ConfigAddPage() {
body: JSON.stringify(kpiData),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
navigate({ to: "/config" });
navigate({
to: "/config",
search: { success: "true", ...(from ? { from } : {}) },
});
} catch (error) {
console.error('Error creating KPI:', error);
throw error;
}
};
const handleCancel = () => {
navigate({ to: "/config" });
navigate({
to: "/config",
search: from ? { from } : undefined,
});
};
return (
@ -83,7 +93,7 @@ function ConfigAddPage() {
>
<Box display="flex" alignItems="center">
<IconButton onClick={handleBack}>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
</IconButton>
<Typography variant="h5" fontWeight="bold" ml={3}>
Neue Kennzahl hinzufügen
@ -93,9 +103,10 @@ function ConfigAddPage() {
<KPIForm
mode="add"
key={Date.now()}
onSave={handleSave}
onCancel={handleCancel}
/>
</Box>
);
}
}

View File

@ -1,5 +1,6 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { Box, Typography, IconButton, Button, CircularProgress, Paper, Divider
import {
Box, Typography, IconButton, Button, CircularProgress, Paper, Divider
} from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { useEffect, useState } from "react";
@ -38,6 +39,7 @@ function KPIDetailPage() {
try {
setLoading(true);
const response = await fetch(`${API_HOST}/api/kpi_setting/${kpiId}`);
if (!response.ok) {
if (response.status === 404) {
setError('KPI not found');
@ -72,7 +74,6 @@ function KPIDetailPage() {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const updatedKennzahl = await response.json();
setKennzahl(updatedKennzahl);
setIsEditing(false);
@ -82,6 +83,7 @@ function KPIDetailPage() {
}
};
const handleCancel = () => {
setIsEditing(false);
};
@ -153,7 +155,7 @@ function KPIDetailPage() {
>
<Box display="flex" alignItems="center">
<IconButton onClick={handleBack}>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
</IconButton>
<Typography variant="h5" fontWeight="bold" ml={3}>
Detailansicht
@ -192,18 +194,13 @@ function KPIDetailPage() {
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Beschreibung
<Typography variant="h6" fontWeight="bold" mb={1}>
Erforderlich:
</Typography>
<Typography variant="body1" color="text.secondary">
{kennzahl.description || "Zurzeit ist die Beschreibung der Kennzahl leer. Klicken Sie auf den Bearbeiten-Button, um die Beschreibung zu ergänzen."}
<Typography variant="body1" sx={{ mb: 2, fontSize: 16 }}>
{kennzahl.mandatory ? 'Ja' : 'Nein'}
</Typography>
<Box mt={2}>
<Typography variant="body2" color="text.secondary">
<strong>Erforderlich:</strong> {kennzahl.mandatory ? 'Ja' : 'Nein'}
</Typography>
</Box>
</Box>
<Divider sx={{ my: 3 }} />
@ -216,28 +213,6 @@ function KPIDetailPage() {
{typeDisplayMapping[kennzahl.type] || kennzahl.type}
</Typography>
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Synonyme & Übersetzungen
</Typography>
<Typography variant="body1" color="text.secondary">
{kennzahl.translation || "Zurzeit gibt es keine Einträge für Synonyme und Übersetzungen der Kennzahl. Klicken Sie auf den Bearbeiten-Button, um die Liste zu ergänzen."}
</Typography>
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Beispiele von Kennzahl
</Typography>
<Typography variant="body1" color="text.secondary">
{kennzahl.example || "Zurzeit gibt es keine Beispiele der Kennzahl. Klicken Sie auf den Bearbeiten-Button, um die Liste zu ergänzen."}
</Typography>
</Box>
</Paper>
</Box>
);
@ -264,7 +239,7 @@ function KPIDetailPage() {
>
<Box display="flex" alignItems="center">
<IconButton onClick={handleBack}>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
</IconButton>
<Typography variant="h5" fontWeight="bold" ml={3}>
Kennzahl bearbeiten
@ -280,4 +255,4 @@ function KPIDetailPage() {
/>
</Box>
);
}
}

View File

@ -3,18 +3,87 @@ import { Box, Button, IconButton, Typography } from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { useNavigate } from "@tanstack/react-router";
import { ConfigTable } from "../components/ConfigTable";
import { API_HOST } from "../util/api";
import Snackbar from "@mui/material/Snackbar";
import MuiAlert from "@mui/material/Alert";
import { useState, useEffect } from "react";
import CircularProgress from "@mui/material/CircularProgress";
import Tooltip from "@mui/material/Tooltip";
export const Route = createFileRoute("/config")({
component: ConfigPage,
validateSearch: (search: Record<string, unknown>): { from?: string } => {
const from = typeof search.from === "string" ? search.from : undefined;
return { from };
validateSearch: (search: Record<string, unknown>): { from?: string; success?: string } => {
return {
from: typeof search.from === "string" ? search.from : undefined,
success: typeof search.success === "string" ? search.success : undefined
};
}
});
function ConfigPage() {
const navigate = useNavigate();
const { from } = Route.useSearch();
const { from, success } = Route.useSearch();
const [snackbarOpen, setSnackbarOpen] = useState(success === "true");
const [snackbarMessage, setSnackbarMessage] = useState<string>("Beispielsätze gespeichert. Jetzt auf -Neu trainieren- klicken oder zuerst weitere Kennzahlen hinzufügen.");
const [trainingRunning, setTrainingRunning] = useState(false);
const [hasUntrainedKPIs, setHasUntrainedKPIs] = useState(false);
const fetchKPISettings = async () => {
try {
const res = await fetch(`${API_HOST}/api/kpi/settings`);
const data = await res.json();
const untrainedExists = data.some((kpi: any) => {
return kpi.is_trained === false;
});
setHasUntrainedKPIs(untrainedExists);
} catch (err) {
console.error("Fehler beim Laden der KPIs:", err);
}
};
useEffect(() => {
fetchKPISettings();
}, []);
useEffect(() => {
if (success === "true") {
setTimeout(() => {
navigate({
to: "/config",
search: from ? { from } : undefined,
replace: true
});
}, 100);
}
}, [success]);
useEffect(() => {
const checkInitialTrainingStatus = async () => {
try {
const res = await fetch(`${API_HOST}/api/spacy/train-status`);
const data = await res.json();
if (data.running) {
setTrainingRunning(true);
pollTrainingStatus();
}
} catch (err) {
console.error("Initiale Trainingsstatus-Abfrage fehlgeschlagen", err);
}
};
checkInitialTrainingStatus();
}, []);
const handleAddNewKPI = () => {
navigate({
@ -31,46 +100,160 @@ function ConfigPage() {
}
};
const handleTriggerTraining = async () => {
setTrainingRunning(true);
try {
const response = await fetch(`${API_HOST}/api/spacy/train`, {
method: "POST",
});
if (!response.ok) throw new Error("Training konnte nicht gestartet werden");
// Erfolgsmeldung erst hier anzeigen
setSnackbarMessage("Training wurde gestartet.");
setSnackbarOpen(true);
pollTrainingStatus(); // jetzt starten
} catch (err) {
console.error(err);
setSnackbarMessage("Fehler beim Starten des Trainings.");
setSnackbarOpen(true);
setTrainingRunning(false);
}
};
const pollTrainingStatus = () => {
const interval = setInterval(async () => {
try {
const res = await fetch(`${API_HOST}/api/spacy/train-status`);
const data = await res.json();
console.log("Trainingsstatus:", data); //Debug-Ausgabe
if (!data.running) {
clearInterval(interval);
console.log("Training abgeschlossen Snackbar wird ausgelöst");
setSnackbarMessage("Training abgeschlossen!");
setSnackbarOpen(true);
setTrainingRunning(false);
fetchKPISettings();
}
} catch (err) {
console.error("Polling-Fehler:", err);
clearInterval(interval);
}
}, 3000);
};
return (
<Box
minHeight="100vh"
width="100vw"
bgcolor="white"
display="flex"
flexDirection="column"
alignItems="center"
pt={3}
pb={4}
>
<>
<Box
width="100%"
minHeight="100vh"
width="100vw"
bgcolor="white"
display="flex"
justifyContent="space-between"
flexDirection="column"
alignItems="center"
px={4}
pt={3}
pb={4}
>
<Box display="flex" alignItems="center">
<IconButton onClick={handleBack}>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
</IconButton>
<Typography variant="h5" fontWeight="bold" ml={3}>
Konfiguration der Kennzahlen
</Typography>
</Box>
<Button
variant="contained"
onClick={handleAddNewKPI}
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
<Box
width="100%"
display="flex"
justifyContent="space-between"
alignItems="center"
px={4}
>
Neue Kennzahl hinzufügen
</Button>
{/* Linke Seite: Zurück & Titel */}
<Box display="flex" alignItems="center">
<IconButton onClick={handleBack}>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
</IconButton>
<Typography variant="h5" fontWeight="bold" ml={3}>
Konfiguration der Kennzahlen
</Typography>
</Box>
<Box display="flex" flexDirection="column" alignItems="flex-end" gap={1}>
<Box display="flex" gap={2}>
{trainingRunning || !hasUntrainedKPIs ? (
<Tooltip title="Alle Kennzahlen sind bereits trainiert.">
<span>
<Button
variant="contained"
onClick={handleTriggerTraining}
disabled
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
>
{trainingRunning ? (
<>
<CircularProgress size={20} sx={{ color: "white", mr: 1 }} />
Wird trainiert...
</>
) : (
"Neu trainieren"
)}
</Button>
</span>
</Tooltip>
) : (
<Button
variant="contained"
onClick={handleTriggerTraining}
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
>
Neu trainieren
</Button>
)}
<Button
variant="contained"
onClick={handleAddNewKPI}
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
>
Neue Kennzahl hinzufügen
</Button>
</Box>
</Box>
</Box>
{/* Tabelle */}
<Box sx={{ width: "100%", mt: 4, display: "flex", justifyContent: "center" }}>
<ConfigTable from={from} trainingRunning={trainingRunning} />
</Box>
</Box>
<Box sx={{ width: "100%", mt: 4, display: "flex", justifyContent: "center" }}>
<ConfigTable from={from} />
</Box>
</Box>
{/* Snackbar */}
<Snackbar
open={snackbarOpen}
autoHideDuration={4000}
onClose={() => setSnackbarOpen(false)}
anchorOrigin={{ vertical: "top", horizontal: "center" }}
>
<MuiAlert
elevation={6}
variant="filled"
onClose={() => setSnackbarOpen(false)}
severity="success"
sx={{ width: "100%" }}
>
{snackbarMessage}
</MuiAlert>
</Snackbar>
</>
);
}

View File

@ -1,13 +1,17 @@
export interface Kennzahl {
id: number;
name: string;
description: string;
mandatory: boolean;
type: string;
translation: string;
example: string;
position: number;
active: boolean;
exampleText?: string;
markedValue?: string;
examples?: {
sentence: string;
value: string;
}[];
is_trained?: boolean;
}
export const typeDisplayMapping: Record<string, string> = {

View File

@ -5,6 +5,9 @@ import { defineConfig } from "vite";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [TanStackRouterVite({ autoCodeSplitting: true }), viteReact()],
build: {
chunkSizeWarningLimit: 1000, // default ist 500
},
test: {
globals: true,
environment: "jsdom",